From a1a6e0c1c0b4be4c5bea0e3233be8bb11e60a079 Mon Sep 17 00:00:00 2001 From: "Czarnowski, Przemyslaw" Date: Fri, 20 Dec 2019 13:44:20 +0100 Subject: Add Process and DeviceMonitor class with dependencies Added DeviceMonitor which watches for ndb device changes. Contains: - Udev library wrappers. - NBDevice object to manage nbd devices in errorless manner. - Process library, which manages process spawning. Change-Id: Iaf3caec56cd6084f1c17ccc5657b9b14c8e82d33 Signed-off-by: Rapkiewicz, Pawel Signed-off-by: Czarnowski, Przemyslaw --- virtual-media/src/main.cpp | 9 +- virtual-media/src/system.hpp | 585 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 virtual-media/src/system.hpp diff --git a/virtual-media/src/main.cpp b/virtual-media/src/main.cpp index 7caae14..666c3a6 100644 --- a/virtual-media/src/main.cpp +++ b/virtual-media/src/main.cpp @@ -1,4 +1,5 @@ #include "logger.hpp" +#include "system.hpp" #include @@ -19,7 +20,8 @@ class App { public: - App(boost::asio::io_context& ioc, sd_bus* custom_bus = nullptr) : ioc(ioc) + App(boost::asio::io_context& ioc, sd_bus* custom_bus = nullptr) : + ioc(ioc), devMonitor(ioc) { if (!custom_bus) { @@ -34,6 +36,10 @@ class App bus->request_name("xyz.openbmc_project.VirtualMedia"); objManager = std::make_shared( *bus, "/xyz/openbmc_project/VirtualMedia"); + + devMonitor.run([](const NBDDevice& device, StateChange change) { + // placeholder for some future actions + }); } private: @@ -41,6 +47,7 @@ class App std::shared_ptr bus; std::shared_ptr objServer; std::shared_ptr objManager; + DeviceMonitor devMonitor; }; int main() diff --git a/virtual-media/src/system.hpp b/virtual-media/src/system.hpp new file mode 100644 index 0000000..ca9d17e --- /dev/null +++ b/virtual-media/src/system.hpp @@ -0,0 +1,585 @@ +#pragma once + +#include "logger.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace udev +{ +#include +struct udev; +struct udev_monitor; +struct udev_device; + +struct udevDeleter +{ + void operator()(udev* desc) const + { + udev_unref(desc); + } +}; + +struct monitorDeleter +{ + void operator()(udev_monitor* desc) const + { + udev_monitor_unref(desc); + }; +}; + +struct deviceDeleter +{ + void operator()(udev_device* desc) const + { + udev_device_unref(desc); + }; +}; +} // namespace udev + +#define NBD_DISCONNECT _IO(0xab, 8) +#define NBD_CLEAR_SOCK _IO(0xab, 4) + +class NBDDevice +{ + public: + enum Value : uint8_t // This is explicite not a class to avoid naming like + // NBDDevice::Device::nbd0 + { + nbd0 = 0, + nbd1 = 1, + nbd2 = 2, + nbd3 = 3, + nbd4 = 4, + nbd5 = 5, + nbd6 = 6, + nbd7 = 7, + nbd8 = 8, + nbd9 = 9, + nbd10 = 10, + unknown = 0xFF + }; + + NBDDevice() = default; + NBDDevice(Value v) : value(v){}; + explicit NBDDevice(const char* nbdName) + { + if (nbdName != nullptr) + { + const auto iter = + std::find(nameMatching.cbegin(), nameMatching.cend(), nbdName); + if (iter != nameMatching.cend()) + { + value = static_cast( + std::distance(nameMatching.cbegin(), iter)); + } + } + } + NBDDevice(const NBDDevice&) = default; + NBDDevice(NBDDevice&&) = default; + + NBDDevice& operator=(const NBDDevice&) = default; + NBDDevice& operator=(NBDDevice&&) = default; + + bool operator==(const NBDDevice& rhs) const + { + return value == rhs.value; + } + bool operator!=(const NBDDevice& rhs) const + { + return value != rhs.value; + } + bool operator<(const NBDDevice& rhs) const + { + return value < rhs.value; + } + explicit operator bool() + { + return (value != unknown); + } + + bool isReady() const + { + if (value == unknown) + { + return false; + } + int fd = open(to_path().c_str(), O_EXCL); + if (fd < 0) + { + return false; + } + close(fd); + return true; + } + + void disconnect() const + { + if (value == unknown) + { + return; + } + + int fd = open(to_path().c_str(), O_RDWR); + + if (fd < 0) + { + LogMsg(Logger::Error, "Couldn't open device ", to_path().c_str()); + return; + } + if (ioctl(fd, NBD_DISCONNECT) < 0) + { + LogMsg(Logger::Debug, "Ioctl failed: \n"); + } + if (ioctl(fd, NBD_CLEAR_SOCK) < 0) + { + LogMsg(Logger::Debug, "Ioctl failed: \n"); + } + close(fd); + } + + std::string to_string() const + { + if (value == unknown) + { + return ""; + } + return nameMatching[static_cast(value)]; + } + + fs::path to_path() const + { + if (value == unknown) + { + return fs::path(); + } + return fs::path("/dev") / + fs::path(nameMatching[static_cast(value)]); + } + + private: + Value value = unknown; + + static const inline std::vector nameMatching = { + "nbd0", "nbd1", "nbd2", "nbd3", "nbd4", "nbd5", + "nbd6", "nbd7", "nbd8", "nbd9", "nbd10"}; +}; + +enum class StateChange +{ + notMonitored, + unknown, + removed, + inserted +}; + +class DeviceMonitor +{ + public: + DeviceMonitor(boost::asio::io_context& ioc) : ioc(ioc), monitorSd(ioc) + { + udev = std::unique_ptr(udev::udev_new()); + if (!udev) + { + throw std::system_error(EFAULT, std::generic_category(), + "Unable to create uDev handler."); + } + + monitor = std::unique_ptr( + udev::udev_monitor_new_from_netlink(udev.get(), "kernel")); + + if (!monitor) + { + throw std::system_error(EFAULT, std::generic_category(), + "Unable to create uDev Monitor handler."); + } + int rc = udev_monitor_filter_add_match_subsystem_devtype( + monitor.get(), "block", "disk"); + + if (rc) + { + throw std::system_error(EFAULT, std::generic_category(), + "Could not apply filters."); + } + rc = udev_monitor_enable_receiving(monitor.get()); + if (rc) + { + throw std::system_error(EFAULT, std::generic_category(), + "Enable receiving failed."); + } + monitorSd.assign(udev_monitor_get_fd(monitor.get())); + } + + DeviceMonitor(const DeviceMonitor&) = delete; + DeviceMonitor(DeviceMonitor&&) = delete; + + DeviceMonitor& operator=(const DeviceMonitor&) = delete; + DeviceMonitor& operator=(DeviceMonitor&&) = delete; + + template + void run(DeviceChangeStateCb callback) + { + boost::asio::spawn(ioc, [this, + callback](boost::asio::yield_context yield) { + boost::system::error_code ec; + while (1) + { + monitorSd.async_wait( + boost::asio::posix::stream_descriptor::wait_read, + yield[ec]); + + std::unique_ptr device = + std::unique_ptr( + udev::udev_monitor_receive_device(monitor.get())); + if (device) + { + const char* devAction = + udev_device_get_action(device.get()); + if (devAction == nullptr) + { + LogMsg(Logger::Error, + "[DeviceMonitor]: Received NULL action."); + continue; + } + if (strcmp(devAction, "change") != 0) + { + continue; + } + + const char* sysname = udev_device_get_sysname(device.get()); + if (sysname == nullptr) + { + LogMsg(Logger::Error, + "[DeviceMonitor]: Received NULL sysname."); + continue; + } + + NBDDevice nbdDevice(sysname); + if (!nbdDevice) + { + continue; + } + + auto monitoredDevice = devices.find(nbdDevice); + if (monitoredDevice == devices.cend()) + { + continue; + } + + const char* sizeStr = + udev_device_get_sysattr_value(device.get(), "size"); + if (sizeStr == nullptr) + { + LogMsg(Logger::Error, + "[DeviceMonitor]: Received NULL size."); + continue; + } + + uint64_t size = 0; + try + { + size = std::stoul(sizeStr, 0, 0); + } + catch (const std::exception& e) + { + LogMsg(Logger::Error, + "[DeviceMonitor]: Could not convert " + "size " + "to integer."); + continue; + } + if (size > 0 && + monitoredDevice->second != StateChange::inserted) + { + LogMsg(Logger::Info, + "[DeviceMonitor]: ", nbdDevice.to_path(), + " inserted."); + monitoredDevice->second = StateChange::inserted; + callback(nbdDevice, StateChange::inserted); + } + else if (size == 0 && + monitoredDevice->second != StateChange::removed) + { + LogMsg(Logger::Info, + "[DeviceMonitor]: ", nbdDevice.to_path(), + " removed."); + monitoredDevice->second = StateChange::removed; + callback(nbdDevice, StateChange::removed); + } + } + } + }); + } + + void addDevice(const NBDDevice& device) + { + LogMsg(Logger::Info, "[DeviceMonitor]: watch on ", device.to_path()); + devices.insert( + std::pair(device, StateChange::unknown)); + } + + StateChange getState(const NBDDevice& device) + { + auto monitoredDevice = devices.find(device); + if (monitoredDevice != devices.cend()) + { + return monitoredDevice->second; + } + return StateChange::notMonitored; + } + + private: + boost::asio::io_context& ioc; + boost::asio::posix::stream_descriptor monitorSd; + + std::unique_ptr udev; + std::unique_ptr monitor; + + boost::container::flat_map devices; +}; + +class Process : public std::enable_shared_from_this +{ + public: + Process(boost::asio::io_context& ioc, const std::string& name, + const NBDDevice& dev) : + ioc(ioc), + pipe(ioc), name(name), dev(dev) + { + } + + template + bool spawn(const std::vector& args, ExitCb&& onExit) + { + std::error_code ec; + LogMsg(Logger::Info, "[Process]: Spawning nbd-client (", args, ")"); + child = boost::process::child( + "/usr/sbin/nbd-client", boost::process::args(args), + (boost::process::std_out & boost::process::std_err) > pipe, ec, + ioc); + + if (ec) + { + LogMsg(Logger::Error, + "[Process]: Error while creating child process: ", ec); + 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) + { + 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) + { + 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()); + + onExit(child.exit_code(), dev.isReady()); + }); + return true; + } + + void stop() + { + boost::asio::spawn(ioc, [this, self = shared_from_this()]( + boost::asio::yield_context yield) { + // The Good + dev.disconnect(); + + // The Ugly (but required) + 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()) + { + LogMsg(Logger::Info, "[Process] Terminate if process doesnt " + "want to exit nicely"); + child.terminate(); + } + }); + } + + private: + boost::asio::io_context& ioc; + boost::process::child child; + boost::process::async_pipe pipe; + std::string name; + const NBDDevice& dev; +}; + +struct UsbGadget +{ + private: + static bool echoToFile(const fs::path& fname, const std::string& content) + { + std::ofstream fileWriter; + fileWriter.exceptions(std::ofstream::failbit | std::ofstream::badbit); + fileWriter.open(fname, std::ios::out | std::ios::app); + fileWriter << content << std::endl; // Make sure for new line and flush + fileWriter.close(); + LogMsg(Logger::Debug, "echo ", content, " > ", fname); + return true; + } + + public: + static int32_t configure(const std::string& name, const NBDDevice& nbd, + StateChange change) + { + return configure(name, nbd.to_path(), change); + } + + static int32_t configure(const std::string& name, const fs::path& path, + StateChange change) + { + LogMsg(Logger::Info, "[App]: Configure USB Gadget (name=", name, + ", path=", path, ", State=", static_cast(change), ")"); + bool success = true; + std::error_code ec; + if (change == StateChange::unknown) + { + LogMsg(Logger::Critical, + "[App]: Change to unknown state is not possible"); + return -1; + } + + const fs::path gadgetDir = + "/sys/kernel/config/usb_gadget/mass-storage-" + name; + const fs::path funcMassStorageDir = + gadgetDir / "functions/mass_storage.usb0"; + const fs::path stringsDir = gadgetDir / "strings/0x409"; + const fs::path configDir = gadgetDir / "configs/c.1"; + const fs::path massStorageDir = configDir / "mass_storage.usb0"; + const fs::path configStringsDir = configDir / "strings/0x409"; + + if (change == StateChange::inserted) + { + try + { + fs::create_directories(gadgetDir); + echoToFile(gadgetDir / "idVendor", "0x1d6b"); + echoToFile(gadgetDir / "idProduct", "0x0104"); + fs::create_directories(stringsDir); + echoToFile(stringsDir / "manufacturer", "OpenBMC"); + echoToFile(stringsDir / "product", "Virtual Media Device"); + fs::create_directories(configStringsDir); + echoToFile(configStringsDir / "configuration", "config 1"); + fs::create_directories(funcMassStorageDir); + fs::create_directories(funcMassStorageDir / "lun.0"); + fs::create_directory_symlink(funcMassStorageDir, + massStorageDir); + echoToFile(funcMassStorageDir / "lun.0/removable", "1"); + echoToFile(funcMassStorageDir / "lun.0/ro", "1"); + echoToFile(funcMassStorageDir / "lun.0/cdrom", "0"); + echoToFile(funcMassStorageDir / "lun.0/file", path); + + for (const auto& port : fs::directory_iterator( + "/sys/bus/platform/devices/1e6a0000.usb-vhub")) + { + if (fs::is_directory(port) && !fs::is_symlink(port) && + !fs::exists(port.path() / "gadget/suspended")) + { + const std::string portId = port.path().filename(); + LogMsg(Logger::Debug, + "Use port : ", port.path().filename()); + echoToFile(gadgetDir / "UDC", portId); + return 0; + } + } + } + catch (fs::filesystem_error& e) + { + // Got error perform cleanup + LogMsg(Logger::Error, "[App]: UsbGadget: ", e.what()); + success = false; + } + catch (std::ofstream::failure& e) + { + // Got error perform cleanup + LogMsg(Logger::Error, "[App]: UsbGadget: ", e.what()); + success = false; + } + } + // StateChange: unknown, notMonitored, inserted were handler + // earlier. We'll get here only for removed, or cleanup + + const std::array dirs = { + massStorageDir.c_str(), funcMassStorageDir.c_str(), + configStringsDir.c_str(), configDir.c_str(), + stringsDir.c_str(), gadgetDir.c_str()}; + for (const char* dir : dirs) + { + fs::remove_all(dir, ec); + if (ec) + { + success = false; + LogMsg(Logger::Error, "[App]: UsbGadget ", ec.message()); + } + } + + if (success) + { + return 0; + } + return -1; + } +}; -- cgit v1.2.3