From dad0aaa9b33a5a217ac115334a94fe299dce9e08 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Fri, 10 Oct 2025 10:01:01 +0200 Subject: Add bluetooth module Can't really do anything yet, but finds the bluetooth adapter and registers an agent to make it pairable. --- src/bt.cc | 953 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/bt.hh | 138 ++++++++ src/looper.hh | 30 ++ src/looper_poll.cc | 176 +++++++--- src/main.cc | 95 +++++- src/signals.cc | 89 +++++ src/signals.hh | 33 ++ src/unique_pipe.cc | 32 ++ src/unique_pipe.hh | 42 +++ 9 files changed, 1539 insertions(+), 49 deletions(-) create mode 100644 src/bt.cc create mode 100644 src/bt.hh create mode 100644 src/signals.cc create mode 100644 src/signals.hh create mode 100644 src/unique_pipe.cc create mode 100644 src/unique_pipe.hh (limited to 'src') diff --git a/src/bt.cc b/src/bt.cc new file mode 100644 index 0000000..6ab7d1d --- /dev/null +++ b/src/bt.cc @@ -0,0 +1,953 @@ +#include "bt.hh" + +#include "cfg.hh" +#include "logger.hh" +#include "looper.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bt { + +namespace { + +class IManager { + public: + virtual ~IManager() = default; + + IManager(IManager const&) = delete; + IManager& operator=(IManager const&) = delete; + + virtual void add_device( + sdbus::ObjectPath const& path, + std::map const& properties) = 0; + virtual void add_adapter( + sdbus::ObjectPath const& path, + std::map const& properties) = 0; + virtual void add_agent_manager(sdbus::ObjectPath const& path) = 0; + + virtual void remove_device(sdbus::ObjectPath const& path) = 0; + virtual void remove_adapter(sdbus::ObjectPath const& path) = 0; + virtual void remove_agent_manager(sdbus::ObjectPath const& path) = 0; + virtual void remove_agent() = 0; + + virtual void update_adapter(sdbus::ObjectPath const& path) = 0; + virtual void update_device(sdbus::ObjectPath const& path) = 0; + + virtual std::vector get_devices( + sdbus::ObjectPath const& adapter_path) = 0; + + virtual void agent_request_pincode( + sdbus::ObjectPath const& device, + std::function)> callback) = 0; + virtual void agent_display_pincode(sdbus::ObjectPath const& device, + std::string const& pincode) = 0; + virtual void agent_request_passkey( + sdbus::ObjectPath const& device, + std::function)> callback) = 0; + virtual void agent_display_passkey(sdbus::ObjectPath const& device, + uint32_t passkey, uint16_t entered) = 0; + virtual void agent_request_confirmation( + sdbus::ObjectPath const& device, uint32_t passkey, + std::function callback) = 0; + virtual void agent_request_authorization( + sdbus::ObjectPath const& device, std::function callback) = 0; + virtual void agent_authorize_service(sdbus::ObjectPath const& device, + std::string const& uuid, + std::function callback) = 0; + virtual void agent_cancel() = 0; + + virtual logger::Logger& logger() = 0; + + protected: + IManager() = default; +}; + +const sdbus::ServiceName kService{"org.bluez"}; +const sdbus::InterfaceName kDeviceInterface{"org.bluez.Device1"}; +const sdbus::InterfaceName kAdapterInterface{"org.bluez.Adapter1"}; +const sdbus::InterfaceName kAgentInterface{"org.bluez.Agent1"}; +const sdbus::InterfaceName kAgentManagerInterface{"org.bluez.AgentManager1"}; +const sdbus::InterfaceName kPropertiesInterface{ + "org.freedesktop.DBus.Properties"}; + +class ObjectManagerProxy { + private: + static inline sdbus::InterfaceName kInterface{ + "org.freedesktop.DBus.ObjectManager"}; + + public: + ObjectManagerProxy(IManager& manager, sdbus::IConnection& connection, + sdbus::ServiceName destination, sdbus::ObjectPath path, + sdbus::ObjectPath filter) + : proxy_(sdbus::createProxy(connection, std::move(destination), + std::move(path))), + manager_(manager), + filter_(std::move(filter)) { + proxy_->uponSignal("InterfacesAdded") + .onInterface(kInterface) + .call([this]( + sdbus::ObjectPath const& path, + std::map> const& + interfaces_and_properties) { + interfaces_added(path, interfaces_and_properties); + }); + + proxy_->uponSignal("InterfacesRemoved") + .onInterface(kInterface) + .call([this](sdbus::ObjectPath const& path, + std::vector const& interfaces) { + this->interfaces_removed(path, interfaces); + }); + + proxy_->callMethodAsync("GetManagedObjects") + .onInterface(kInterface) + .uponReplyInvoke( + [this]( + std::optional err, + std::map>> const& objects) { + if (err.has_value()) { + if (err->getName() == + "org.freedesktop.DBus.Error.ServiceUnknown") { + manager_.logger().info("Waiting for server to start ..."); + } else { + manager_.logger().err(std::format("GetManagedObjects {}: {}", + std::string(err->getName()), + err->getMessage())); + } + return; + } + + for (auto const& pair : objects) { + interfaces_added(pair.first, pair.second); + } + }); + } + + ObjectManagerProxy(ObjectManagerProxy const&) = delete; + ObjectManagerProxy& operator=(ObjectManagerProxy const&) = delete; + + virtual ~ObjectManagerProxy() { proxy_->unregister(); } + + private: + void interfaces_added( + sdbus::ObjectPath const& object_path, + std::map> const& + interfaces_and_properties) { + if (!object_path.starts_with(filter_)) { + manager_.logger().dbg( + std::format("ignoring: {}", std::string(object_path))); + return; + } + + auto it = interfaces_and_properties.find(kDeviceInterface); + if (it != interfaces_and_properties.end()) { + manager_.add_device(object_path, it->second); + } + it = interfaces_and_properties.find(kAdapterInterface); + if (it != interfaces_and_properties.end()) { + manager_.add_adapter(object_path, it->second); + } + it = interfaces_and_properties.find(kAgentManagerInterface); + if (it != interfaces_and_properties.end()) { + manager_.add_agent_manager(object_path); + } + } + + void interfaces_removed(sdbus::ObjectPath const& object_path, + std::vector const& interfaces) { + if (!object_path.starts_with(filter_)) + return; + + for (auto const& interface : interfaces) { + if (interface == kDeviceInterface) { + manager_.remove_device(object_path); + } else if (interface == kAdapterInterface) { + manager_.remove_adapter(object_path); + } else if (interface == kAgentManagerInterface) { + manager_.remove_agent_manager(object_path); + } + } + } + + std::unique_ptr proxy_; + IManager& manager_; + sdbus::ObjectPath filter_; +}; + +class BaseProxy : public sdbus::ProxyInterfaces { + public: + virtual ~BaseProxy() { unregisterProxy(); } + + protected: + BaseProxy(sdbus::IConnection& connection, sdbus::ObjectPath const& path, + std::map const& names) + : sdbus::ProxyInterfaces(connection, kService, + path), + names_(names) {} + + virtual void update(size_t field, sdbus::Variant const& value) = 0; + virtual void notify_updated() = 0; + + void init(std::map const& properties) { + for (auto const& pair : properties) { + auto it = names_.find(pair.first); + if (it != names_.end()) { + update(it->second, pair.second); + } + } + + registerProxy(); + } + + private: + void onPropertiesChanged( + const sdbus::InterfaceName& interface_name, + const std::map& changed, + const std::vector& invalidated) override { + if (interface_name != kAdapterInterface) + return; + ++waiting_; + for (auto const& pair : changed) { + auto it = names_.find(pair.first); + if (it != names_.end()) { + updated_ = true; + update(it->second, pair.second); + } + } + for (auto const& property_name : invalidated) { + auto it = names_.find(property_name); + if (it != names_.end()) { + ++waiting_; + GetAsync(interface_name, property_name, + // NOLINTNEXTLINE(performance-unnecessary-value-param) + [this, field = it->second](std::optional err, + sdbus::Variant const& value) { + if (!err.has_value()) { + updated_ = true; + update(field, value); + } + if (--waiting_ == 0 && updated_) { + notify_updated(); + } + }); + } + } + if (--waiting_ == 0 && updated_) { + notify_updated(); + } + } + + std::map const& names_; + uint8_t waiting_{0}; + bool updated_{false}; +}; + +class DeviceProxy : public BaseProxy, public Device { + private: + static inline const std::map kNames{ + {sdbus::PropertyName{"Address"}, 0}, {sdbus::PropertyName{"Name"}, 1}, + {sdbus::PropertyName{"Alias"}, 2}, {sdbus::PropertyName{"Paired"}, 3}, + {sdbus::PropertyName{"Connected"}, 4}, {sdbus::PropertyName{"Adapter"}, 5}, + }; + + public: + DeviceProxy(IManager& manager, sdbus::IConnection& connection, + sdbus::ObjectPath const& path, + std::map const& properties) + : BaseProxy(connection, path, kNames), manager_(manager) { + init(properties); + } + + [[nodiscard]] + std::string const& address() const override { + return address_; + } + + [[nodiscard]] + std::string const& name() const override { + if (alias_.has_value() && !alias_->empty()) + return alias_.value(); + if (name_.has_value() && !name_->empty()) + return name_.value(); + return address_; + } + + [[nodiscard]] + bool paired() const override { + return paired_; + } + + [[nodiscard]] + bool connected() const override { + return connected_; + } + + [[nodiscard]] + sdbus::ObjectPath const& adapter() const { + return adapter_; + } + + private: + void update(size_t field, sdbus::Variant const& value) override { + switch (field) { + case 0: + address_ = value.get(); + break; + case 1: + name_ = value.isEmpty() + ? std::nullopt + : std::optional(value.get()); + break; + case 2: + alias_ = value.isEmpty() + ? std::nullopt + : std::optional(value.get()); + break; + case 3: + paired_ = value.get(); + break; + case 4: + connected_ = value.get(); + break; + case 5: + adapter_ = value.get(); + break; + default: + std::unreachable(); + } + } + + void notify_updated() override { + manager_.update_device(getProxy().getObjectPath()); + } + + IManager& manager_; + std::string address_; + std::optional name_; + std::optional alias_; + bool connected_{false}; + bool paired_{false}; + sdbus::ObjectPath adapter_; +}; + +class AdapterProxy : public BaseProxy, public Adapter { + private: + static inline const std::map kNames{ + {sdbus::PropertyName{"Address"}, 0}, + {sdbus::PropertyName{"Name"}, 1}, + {sdbus::PropertyName{"Alias"}, 2}, + {sdbus::PropertyName{"Discoverable"}, 3}, + {sdbus::PropertyName{"Pairable"}, 4}, + {sdbus::PropertyName{"Pairing"}, 5}, + }; + + public: + AdapterProxy(IManager& manager, sdbus::IConnection& connection, + sdbus::ObjectPath const& path, + std::map const& properties) + : BaseProxy(connection, path, kNames), manager_(manager) { + init(properties); + } + + [[nodiscard]] + std::string const& address() const override { + return address_; + } + + [[nodiscard]] + std::string const& name() const override { + if (!alias_.empty()) + return alias_; + if (!name_.empty()) + return name_; + return address_; + } + + [[nodiscard]] + bool discoverable() const override { + return discoverable_; + } + + [[nodiscard]] + bool pairable() const override { + return pairable_; + } + + [[nodiscard]] + bool pairing() const override { + return pairing_; + } + + [[nodiscard]] + std::vector devices() const override { + return manager_.get_devices(getProxy().getObjectPath()); + } + + private: + void update(size_t field, sdbus::Variant const& value) override { + switch (field) { + case 0: + address_ = value.get(); + break; + case 1: + name_ = value.get(); + break; + case 2: + alias_ = value.get(); + break; + case 3: + discoverable_ = value.get(); + break; + case 4: + pairable_ = value.get(); + manager_.logger().dbg(std::format("pairable: {}", pairable_)); + break; + case 5: + pairing_ = value.get(); + break; + default: + std::unreachable(); + } + } + + void notify_updated() override { + manager_.update_adapter(getProxy().getObjectPath()); + } + + IManager& manager_; + std::string address_; + std::string name_; + std::string alias_; + bool discoverable_{false}; + bool pairable_{false}; + bool pairing_{false}; +}; + +class AgentManagerProxy { + public: + AgentManagerProxy(sdbus::IConnection& connection, + sdbus::ObjectPath const& path) + : proxy_(sdbus::createProxy(connection, kService, path)) {} + + void register_agent( + sdbus::ObjectPath const& path, std::string const& capability, + std::function err)> callback) { + proxy_->callMethodAsync("RegisterAgent") + .onInterface(kAgentManagerInterface) + .withArguments(path, capability) + .uponReplyInvoke(std::move(callback)); + } + + void unregister_agent_sync(sdbus::ObjectPath const& path) { + proxy_->callMethod("UnregisterAgent") + .onInterface(kAgentManagerInterface) + .withArguments(path); + } + + [[nodiscard]] + sdbus::ObjectPath const& object_path() const { + return proxy_->getObjectPath(); + } + + private: + std::unique_ptr proxy_; +}; + +class AgentObject { + private: + static inline const sdbus::ObjectPath kObjectPath{"/org/bluez/agent"}; + static inline const sdbus::Error::Name kErrorCanceled{ + "org.bluez.Error.Canceled"}; + static inline const sdbus::Error::Name kErrorRejected{ + "org.bluez.Error.Rejected"}; + + public: + AgentObject(IManager& manager, sdbus::IConnection& connection) + : manager_(manager), + object_(sdbus::createObject(connection, kObjectPath)) { + object_ + ->addVTable( + sdbus::registerMethod("Release").withNoReply().implementedAs( + [this]() { manager_.remove_agent(); }), + sdbus::registerMethod("RequestPinCode") + .withInputParamNames("device") + .withOutputParamNames("pincode") + .implementedAs([this](sdbus::Result&& result, + sdbus::ObjectPath const& device) { + cancel_pending(); + pending_pincode_ = std::move(result); + manager_.agent_request_pincode(device, [this](auto pincode) { + if (!pending_pincode_.has_value()) + return; + if (pincode.has_value()) { + pending_pincode_->returnResults(pincode.value()); + } else { + pending_pincode_->returnError( + sdbus::Error{kErrorRejected}); + } + pending_pincode_ = std::nullopt; + }); + }), + sdbus::registerMethod("DisplayPinCode") + .withInputParamNames("device", "pincode") + .implementedAs([this](sdbus::ObjectPath const& device, + std::string const& pincode) { + manager_.agent_display_pincode(device, pincode); + }), + sdbus::registerMethod("RequestPasskey") + .withInputParamNames("device") + .withOutputParamNames("passkey") + .implementedAs([this](sdbus::Result&& result, + sdbus::ObjectPath const& device) { + cancel_pending(); + pending_passkey_ = std::move(result); + manager_.agent_request_passkey(device, [this](auto passkey) { + if (!pending_passkey_.has_value()) + return; + if (passkey.has_value()) { + pending_passkey_->returnResults(passkey.value()); + } else { + pending_passkey_->returnError( + sdbus::Error{kErrorRejected}); + } + pending_passkey_ = std::nullopt; + }); + }), + sdbus::registerMethod("DisplayPasskey") + .withInputParamNames("device", "passkey", "entered") + .implementedAs([this](sdbus::ObjectPath const& device, + uint32_t passkey, uint16_t entered) { + manager_.agent_display_passkey(device, passkey, entered); + }), + sdbus::registerMethod("RequestConfirmation") + .withInputParamNames("device", "passkey") + .withOutputParamNames("result") + .implementedAs([this](sdbus::Result&& result, + sdbus::ObjectPath const& device, + uint32_t passkey) { + cancel_pending(); + pending_confirm_ = std::move(result); + manager_.agent_request_confirmation( + device, passkey, [this](auto ok) { + if (!pending_confirm_.has_value()) + return; + if (ok) { + pending_confirm_->returnResults({}); + } else { + pending_confirm_->returnError( + sdbus::Error{kErrorRejected}); + } + pending_confirm_ = std::nullopt; + }); + }), + sdbus::registerMethod("RequestAuthorization") + .withInputParamNames("device") + .withOutputParamNames("result") + .implementedAs([this](sdbus::Result&& result, + sdbus::ObjectPath const& device) { + cancel_pending(); + pending_confirm_ = std::move(result); + manager_.agent_request_authorization(device, [this](auto ok) { + if (!pending_confirm_.has_value()) + return; + if (ok) { + pending_confirm_->returnResults({}); + } else { + pending_confirm_->returnError( + sdbus::Error{kErrorRejected}); + } + pending_confirm_ = std::nullopt; + }); + }), + sdbus::registerMethod("AuthorizeService") + .withInputParamNames("device", "uuid") + .withOutputParamNames("result") + .implementedAs([this](sdbus::Result&& result, + sdbus::ObjectPath const& device, + std::string const& uuid) { + cancel_pending(); + pending_confirm_ = std::move(result); + manager_.agent_authorize_service( + device, uuid, [this](auto ok) { + if (!pending_confirm_.has_value()) + return; + if (ok) { + pending_confirm_->returnResults({}); + } else { + pending_confirm_->returnError( + sdbus::Error{kErrorRejected}); + } + pending_confirm_ = std::nullopt; + }); + }), + sdbus::registerMethod("Cancel").withNoReply().implementedAs( + [this]() { + cancel_pending(); + manager_.agent_cancel(); + })) + .forInterface(kAgentInterface); + } + + [[nodiscard]] + sdbus::ObjectPath const& object_path() const { + return object_->getObjectPath(); + } + + private: + void cancel_pending() { + if (pending_pincode_.has_value()) { + pending_pincode_->returnError(sdbus::Error{kErrorCanceled}); + pending_pincode_ = std::nullopt; + } + if (pending_passkey_.has_value()) { + pending_passkey_->returnError(sdbus::Error{kErrorCanceled}); + pending_passkey_ = std::nullopt; + } + if (pending_confirm_.has_value()) { + pending_confirm_->returnError(sdbus::Error{kErrorCanceled}); + pending_confirm_ = std::nullopt; + } + } + + IManager& manager_; + std::unique_ptr object_; + + std::optional> pending_pincode_; + std::optional> pending_passkey_; + std::optional> pending_confirm_; +}; + +class ManagerImpl : public Manager, public IManager, looper::Hook { + public: + ManagerImpl(logger::Logger& logger, cfg::Config const& cfg, + looper::Looper& looper, Manager::Delegate& delegate) + : logger_(logger), + cfg_(cfg), + looper_(looper), + delegate_(delegate), + conn_(sdbus::createSystemBusConnection()) { + looper_.add_hook(this); + + root_proxy_ = std::make_unique( + *this, *conn_, kService, sdbus::ObjectPath{"/"}, + sdbus::ObjectPath{"/org/bluez"}); + } + + ~ManagerImpl() override { + if (agent_ && agent_manager_proxy_) { + agent_manager_proxy_->unregister_agent_sync(agent_->object_path()); + } + looper_.remove_hook(this); + } + + Adapter* adapter() override { + if (primary_adapter_.empty()) + return nullptr; + return adapters_.at(primary_adapter_).get(); + } + + private: + void add_device(sdbus::ObjectPath const& path, + std::map const& + properties) override { + logger_.dbg(std::format("Add device: {}", std::string(path))); + auto [it, inserted] = devices_.emplace( + path, std::make_unique(*this, *conn_, path, properties)); + + auto& device = *it->second; + if (device.adapter() == primary_adapter_) { + delegate_.added_device(device); + } + } + + void add_adapter(sdbus::ObjectPath const& path, + std::map const& + properties) override { + logger_.dbg(std::format("Add adapter: {}", std::string(path))); + adapters_.emplace( + path, std::make_unique(*this, *conn_, path, properties)); + + if (primary_adapter_.empty()) { + update_primary_adapter(); + } + } + + void add_agent_manager(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Add agent manager: {}", std::string(path))); + if (agent_manager_proxy_) + return; + + agent_manager_proxy_ = std::make_unique(*conn_, path); + agent_ = std::make_unique(*this, *conn_); + agent_manager_proxy_->register_agent( + agent_->object_path(), "", [this](std::optional err) { + if (err.has_value()) { + agent_.reset(); + logger_.warn(std::format("Failed to register agent: {} {}", + std::string(err->getName()), + err->getMessage())); + } else { + logger_.info("Registered agent"); + } + }); + } + + void remove_device(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Remove device: {}", std::string(path))); + auto it = devices_.find(path); + if (it == devices_.end()) + return; + + auto& device = *it->second; + if (device.adapter() == primary_adapter_) { + auto address = device.address(); + devices_.erase(it); + delegate_.removed_device(address); + } else { + devices_.erase(it); + } + } + + void remove_adapter(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Remove adapter: {}", std::string(path))); + auto it = adapters_.find(path); + if (it != adapters_.end()) + adapters_.erase(it); + + if (path == primary_adapter_) { + update_primary_adapter(); + } + } + + void remove_agent_manager(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Remove agemtn manager: {}", std::string(path))); + + if (!agent_manager_proxy_) + return; + if (agent_manager_proxy_->object_path() != path) + return; + + agent_manager_proxy_.reset(); + agent_.reset(); + } + + void update_device(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Update device: {}", std::string(path))); + + auto& device = *devices_.at(path); + if (device.adapter() == primary_adapter_) { + delegate_.updated_device(device); + } + } + + void update_adapter(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Update adapter: {}", std::string(path))); + + if (path == primary_adapter_) { + delegate_.updated_adapter(*adapters_.at(path)); + } + } + + std::vector get_devices( + sdbus::ObjectPath const& adapter_path) override { + std::vector ret; + ret.reserve(devices_.size()); + for (auto& pair : devices_) { + if (pair.second->adapter() == adapter_path) { + ret.push_back(pair.second.get()); + } + } + return ret; + } + + [[nodiscard]] + logger::Logger& logger() override { + return logger_; + } + + void remove_agent() override { + logger_.info("Remove agent"); + agent_ = nullptr; + } + + void agent_request_pincode( + sdbus::ObjectPath const& device, + std::function)> callback) override { + auto it = devices_.find(device); + if (it == devices_.end() || it->second->adapter() != primary_adapter_) { + callback(std::nullopt); + return; + } + delegate_.agent_request_pincode(*it->second, std::move(callback)); + } + + void agent_display_pincode(sdbus::ObjectPath const& device, + std::string const& pincode) override { + auto it = devices_.find(device); + if (it == devices_.end() || it->second->adapter() != primary_adapter_) + return; + delegate_.agent_display_pincode(*it->second, pincode); + } + + void agent_request_passkey( + sdbus::ObjectPath const& device, + std::function)> callback) override { + auto it = devices_.find(device); + if (it == devices_.end() || it->second->adapter() != primary_adapter_) { + callback(std::nullopt); + return; + } + delegate_.agent_request_passkey(*it->second, std::move(callback)); + } + + void agent_display_passkey(sdbus::ObjectPath const& device, uint32_t passkey, + uint16_t entered) override { + auto it = devices_.find(device); + if (it == devices_.end() || it->second->adapter() != primary_adapter_) + return; + delegate_.agent_display_passkey(*it->second, passkey, entered); + } + + void agent_request_confirmation(sdbus::ObjectPath const& device, + uint32_t passkey, + std::function callback) override { + auto it = devices_.find(device); + if (it == devices_.end() || it->second->adapter() != primary_adapter_) { + callback(false); + return; + } + delegate_.agent_request_confirmation(*it->second, passkey, + std::move(callback)); + } + + void agent_request_authorization( + sdbus::ObjectPath const& device, + std::function callback) override { + auto it = devices_.find(device); + if (it == devices_.end() || it->second->adapter() != primary_adapter_) { + callback(false); + return; + } + delegate_.agent_request_authorization(*it->second, std::move(callback)); + } + + void agent_authorize_service(sdbus::ObjectPath const& device, + std::string const& uuid, + std::function callback) override { + auto it = devices_.find(device); + if (it == devices_.end() || it->second->adapter() != primary_adapter_) { + callback(false); + return; + } + delegate_.agent_authorize_service(*it->second, uuid, std::move(callback)); + } + + void agent_cancel() override { delegate_.agent_cancel(); } + + void before(looper::HookHelper& helper) override { + auto data = conn_->getEventLoopPollData(); + + uint8_t events = 0; + // NOLINTNEXTLINE(misc-include-cleaner) + if (data.events & (POLLIN | POLLPRI | POLLHUP)) + events |= looper::EVENT_READ; + // NOLINTNEXTLINE(misc-include-cleaner) + if (data.events & POLLOUT) + events |= looper::EVENT_WRITE; + // NOLINTNEXTLINE(misc-include-cleaner) + if (data.events & (POLLERR | POLLNVAL)) + events |= looper::EVENT_ERROR; + + if (data.fd != -1 && events != 0) + helper.monitor(data.fd, events); + + helper.monitor(data.eventFd, looper::EVENT_READ); + + auto timeout = data.getRelativeTimeout(); + if (timeout != std::chrono::microseconds::max()) { + helper.timeout( + std::chrono::duration_cast(timeout)); + } + } + + void after() override { + while (conn_->processPendingEvent()) + ; + } + + void update_primary_adapter() { + bool const was_empty = primary_adapter_.empty(); + primary_adapter_.clear(); + + auto cfg_adapter = cfg_.get("bluetooth.adapter"); + if (cfg_adapter.has_value()) { + for (auto const& pair : adapters_) { + if (pair.second->address() == cfg_adapter.value()) { + primary_adapter_ = pair.first; + delegate_.new_adapter(pair.second.get()); + return; + } + } + } else { + // Pick first (map is sorted) that is pairable. + for (auto const& pair : adapters_) { + if (pair.second->pairable()) { + primary_adapter_ = pair.first; + delegate_.new_adapter(pair.second.get()); + return; + } + } + } + + if (!was_empty) + delegate_.new_adapter(nullptr); + } + + logger::Logger& logger_; + cfg::Config const& cfg_; + looper::Looper& looper_; + Manager::Delegate& delegate_; + std::unique_ptr conn_; + std::unique_ptr root_proxy_; + std::unique_ptr agent_manager_proxy_; + std::unique_ptr agent_; + std::map> devices_; + std::map> adapters_; + sdbus::ObjectPath primary_adapter_; +}; + +} // namespace + +std::unique_ptr create_manager(logger::Logger& logger, + cfg::Config const& cfg, + looper::Looper& looper, + Manager::Delegate& delegate) { + return std::make_unique(logger, cfg, looper, delegate); +} + +} // namespace bt diff --git a/src/bt.hh b/src/bt.hh new file mode 100644 index 0000000..53d3919 --- /dev/null +++ b/src/bt.hh @@ -0,0 +1,138 @@ +#ifndef BT_HH +#define BT_HH + +#include +#include +#include +#include +#include +#include + +namespace logger { +class Logger; +} // namespace logger + +namespace looper { +class Looper; +} // namespace looper + +namespace cfg { +class Config; +} // namespace cfg + +namespace bt { + +class Device; + +class Adapter { + public: + virtual ~Adapter() = default; + + [[nodiscard]] + virtual std::string const& address() const = 0; + + [[nodiscard]] + virtual std::string const& name() const = 0; + + [[nodiscard]] + virtual bool discoverable() const = 0; + + [[nodiscard]] + virtual bool pairable() const = 0; + + [[nodiscard]] + virtual bool pairing() const = 0; + + [[nodiscard]] + virtual std::vector devices() const = 0; + + protected: + Adapter() = default; + Adapter(Adapter const&) = delete; + Adapter& operator=(Adapter const&) = delete; +}; + +class Device { + public: + virtual ~Device() = default; + + [[nodiscard]] + virtual std::string const& address() const = 0; + + [[nodiscard]] + virtual std::string const& name() const = 0; + + [[nodiscard]] + virtual bool paired() const = 0; + + [[nodiscard]] + virtual bool connected() const = 0; + + protected: + Device() = default; + Device(Device const&) = delete; + Device& operator=(Device const&) = delete; +}; + +class Manager { + public: + virtual ~Manager() = default; + + class Delegate { + public: + virtual ~Delegate() = default; + + // Called when "primary" adapter changes from one to another. + virtual void new_adapter(Adapter* /* adapter */) {} + // Called when "primary" adapter changes a property. + virtual void updated_adapter(Adapter& /* adapter */) {} + + // Called when a new device is added to adapter. + virtual void added_device(Device& /* device */) {} + // Called when a device was removed from adapter. + virtual void removed_device(std::string const& /* address */) {} + // Called when device changes a property. + virtual void updated_device(Device& /* device */) {} + + virtual void agent_request_pincode( + Device& device, + std::function)> callback) = 0; + virtual void agent_display_pincode(Device& device, + std::string const& pincode) = 0; + virtual void agent_request_passkey( + Device& device, + std::function)> callback) = 0; + virtual void agent_display_passkey(Device& device, uint32_t passkey, + uint16_t entered) = 0; + virtual void agent_request_confirmation( + Device& device, uint32_t passkey, + std::function callback) = 0; + virtual void agent_request_authorization( + Device& device, std::function callback) = 0; + virtual void agent_authorize_service( + Device& device, std::string const& uuid, + std::function callback) = 0; + virtual void agent_cancel() = 0; + + protected: + Delegate() = default; + }; + + // Returns null if there are no adapters. + // Otherwise, returns the "primary" adapter. + virtual Adapter* adapter() = 0; + + protected: + Manager() = default; + Manager(Manager const&) = delete; + Manager& operator=(Manager const&) = delete; +}; + +std::unique_ptr create_manager(logger::Logger& logger, + cfg::Config const& cfg, + looper::Looper& looper, + Manager::Delegate& delegate); + +} // namespace bt + +#endif // BT_HH diff --git a/src/looper.hh b/src/looper.hh index 2f4c672..092dfe5 100644 --- a/src/looper.hh +++ b/src/looper.hh @@ -1,6 +1,7 @@ #ifndef LOOPER_HH #define LOOPER_HH +#include #include #include #include @@ -15,6 +16,8 @@ constexpr static uint8_t EVENT_READ = 1; constexpr static uint8_t EVENT_WRITE = 2; constexpr static uint8_t EVENT_ERROR = 4; +class Hook; + class Looper { public: virtual ~Looper() = default; @@ -32,12 +35,39 @@ class Looper { virtual bool run(logger::Logger& logger) = 0; virtual void quit() = 0; + virtual void add_hook(Hook* hook) = 0; + virtual void remove_hook(Hook* hook) = 0; + protected: Looper() = default; Looper(Looper const&) = delete; Looper& operator=(Looper const&) = delete; }; +class HookHelper { + public: + virtual ~HookHelper() = default; + + virtual void monitor(int fd, uint8_t events) = 0; + virtual void timeout(std::chrono::milliseconds timeout) = 0; + + protected: + HookHelper() = default; + HookHelper(HookHelper const&) = delete; + HookHelper& operator=(HookHelper const&) = delete; +}; + +class Hook { + public: + virtual ~Hook() = default; + + virtual void before(HookHelper& helper) = 0; + virtual void after() = 0; + + protected: + Hook() = default; +}; + [[nodiscard]] std::unique_ptr create(); diff --git a/src/looper_poll.cc b/src/looper_poll.cc index 2fed7d2..e6bf2cd 100644 --- a/src/looper_poll.cc +++ b/src/looper_poll.cc @@ -21,6 +21,64 @@ namespace looper { namespace { +inline short events_looper2poll(uint8_t events) { + short ret = 0; + if (events & EVENT_READ) + // NOLINTNEXTLINE(misc-include-cleaner) + ret |= POLLIN | POLLPRI; + if (events & EVENT_WRITE) + // NOLINTNEXTLINE(misc-include-cleaner) + ret |= POLLOUT; + return ret; +} + +inline uint8_t events_poll2looper(short events) { + uint8_t ret = 0; + // NOLINTNEXTLINE(misc-include-cleaner) + if (events & (POLLIN | POLLPRI | POLLHUP)) + ret |= EVENT_READ; + // NOLINTNEXTLINE(misc-include-cleaner) + if (events & POLLOUT) + ret |= EVENT_WRITE; + // NOLINTNEXTLINE(misc-include-cleaner) + if (events & (POLLERR | POLLNVAL)) + ret |= EVENT_ERROR; + return ret; +} + +class HookHelperImpl : public HookHelper { + public: + // NOLINTNEXTLINE(misc-include-cleaner) + HookHelperImpl(std::vector& pollfd, int& timeout) + : pollfd_(pollfd), timeout_(timeout) {} + + void monitor(int fd, uint8_t events) override { + // NOLINTNEXTLINE(misc-include-cleaner) + struct pollfd tmp; + tmp.fd = fd; + tmp.events = events_looper2poll(events); + pollfd_.push_back(tmp); + } + + void timeout(std::chrono::milliseconds timeout) override { + int new_timeout; + if (timeout.count() <= std::numeric_limits::max()) { + new_timeout = static_cast(timeout.count()); + } else { + new_timeout = std::numeric_limits::max(); + } + if (timeout_ == -1) { + timeout_ = new_timeout; + } else { + timeout_ = std::min(timeout_, new_timeout); + } + } + + private: + std::vector& pollfd_; + int& timeout_; +}; + class LooperPoll : public Looper { public: LooperPoll() = default; @@ -91,17 +149,31 @@ class LooperPoll : public Looper { // NOLINTNEXTLINE(misc-include-cleaner) std::vector pollfd; pollfd.reserve(entry_.size()); - auto it = entry_.begin(); - while (it != entry_.end()) { - if (it->second.delete_) { - it = entry_.erase(it); - } else { - // NOLINTNEXTLINE(misc-include-cleaner) - struct pollfd tmp; - tmp.fd = it->first; - tmp.events = events_looper2poll(it->second.events_); - pollfd.push_back(tmp); - ++it; + { + auto it = entry_.begin(); + while (it != entry_.end()) { + if (it->second.delete_) { + it = entry_.erase(it); + } else { + // NOLINTNEXTLINE(misc-include-cleaner) + struct pollfd tmp; + tmp.fd = it->first; + tmp.events = events_looper2poll(it->second.events_); + pollfd.push_back(tmp); + ++it; + } + } + } + if (!hooks_.empty()) { + HookHelperImpl helper(pollfd, timeout); + auto it = hooks_.begin(); + while (it != hooks_.end()) { + if (it->delete_) { + it = hooks_.erase(it); + } else { + it->hook_->before(helper); + ++it; + } } } // NOLINTNEXTLINE(misc-include-cleaner) @@ -112,21 +184,36 @@ class LooperPoll : public Looper { logger.err(std::format("Poll failed: {}", strerror(errno))); return false; } - for (auto it2 = pollfd.begin(); active; ++it2) { - if (it2->revents == 0) - continue; - --active; - auto events = events_poll2looper(it2->revents); - if (events) { - it = entry_.find(it2->fd); - if (!it->second.delete_) { - events &= (it->second.events_ | EVENT_ERROR); - if (events) { - it->second.callback_(events); + { + for (auto it = pollfd.begin(); active; ++it) { + if (it->revents == 0) + continue; + --active; + auto events = events_poll2looper(it->revents); + if (events) { + auto it2 = entry_.find(it->fd); + if (it2 == entry_.end()) + break; + if (!it2->second.delete_) { + events &= (it2->second.events_ | EVENT_ERROR); + if (events) { + it2->second.callback_(events); + } } } } } + { + auto it = hooks_.begin(); + while (it != hooks_.end()) { + if (it->delete_) { + it = hooks_.erase(it); + } else { + it->hook_->after(); + ++it; + } + } + } } // Reset quit_ so run() can be called again quit_ = false; @@ -165,6 +252,18 @@ class LooperPoll : public Looper { } } + void add_hook(Hook* hook) override { + assert(hook); + hooks_.emplace_back(hook); + } + + void remove_hook(Hook* hook) override { + auto it = std::ranges::find_if( + hooks_, [hook](auto const& entry) { return entry.hook_ == hook; }); + if (it != hooks_.end()) + it->delete_ = true; + } + private: struct Entry { uint8_t events_; @@ -174,6 +273,13 @@ class LooperPoll : public Looper { explicit Entry(uint8_t events) : events_(events) {} }; + struct HookEntry { + Hook* hook_; + bool delete_{false}; + + explicit HookEntry(Hook* hook) : hook_(hook) {} + }; + struct Scheduled { std::function callback_; uint32_t id_; @@ -184,31 +290,6 @@ class LooperPoll : public Looper { : callback_(std::move(callback)), id_(id), target_(target) {} }; - static short events_looper2poll(uint8_t events) { - short ret = 0; - if (events & EVENT_READ) - // NOLINTNEXTLINE(misc-include-cleaner) - ret |= POLLIN | POLLPRI; - if (events & EVENT_WRITE) - // NOLINTNEXTLINE(misc-include-cleaner) - ret |= POLLOUT; - return ret; - } - - static uint8_t events_poll2looper(short events) { - uint8_t ret = 0; - // NOLINTNEXTLINE(misc-include-cleaner) - if (events & (POLLIN | POLLPRI | POLLHUP)) - ret |= EVENT_READ; - // NOLINTNEXTLINE(misc-include-cleaner) - if (events & POLLOUT) - ret |= EVENT_WRITE; - // NOLINTNEXTLINE(misc-include-cleaner) - if (events & (POLLERR | POLLNVAL)) - ret |= EVENT_ERROR; - return ret; - } - uint32_t next_schedule_id() { while (true) { uint32_t ret = next_schedule_id_++; @@ -226,6 +307,7 @@ class LooperPoll : public Looper { std::unordered_map entry_; uint32_t next_schedule_id_{1}; std::deque scheduled_; + std::vector hooks_; }; } // namespace diff --git a/src/main.cc b/src/main.cc index 54f3d0a..6883f30 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,15 +1,21 @@ #include "args.hh" +#include "bt.hh" #include "cfg.hh" #include "config.h" #include "http.hh" #include "logger.hh" #include "looper.hh" +#include "signals.hh" #include +#include #include #include +#include #include #include +#include +#include #include #include #include @@ -40,12 +46,97 @@ class HttpServerDelegate : public http::Server::Delegate { } }; +class BluetoothManagerDelegate : public bt::Manager::Delegate { + public: + explicit BluetoothManagerDelegate(logger::Logger& logger) : logger_(logger) {} + + void new_adapter(bt::Adapter* adapter) override { + if (adapter) { + logger_.info(std::format("New adapter: {} [{}]", adapter->name(), + adapter->address())); + } else { + logger_.info("No adapter"); + } + } + + void added_device(bt::Device& device) override { + logger_.info( + std::format("New device: {} [{}]", device.name(), device.address())); + } + + void removed_device(std::string const& address) override { + logger_.info(std::format("Remove device: [{}]", address)); + } + + void agent_request_pincode( + bt::Device& device, + std::function)> callback) override { + logger_.info(std::format("Device request pincode: {}", device.name())); + callback(std::nullopt); + } + + void agent_display_pincode(bt::Device& device, + std::string const& pincode) override { + logger_.info(std::format("Device pincode: {} {}", device.name(), pincode)); + } + + void agent_request_passkey( + bt::Device& device, + std::function)> callback) override { + logger_.info(std::format("Device request passkey: {}", device.name())); + callback(std::nullopt); + } + + void agent_display_passkey(bt::Device& device, uint32_t passkey, + uint16_t /* entered */) override { + logger_.info(std::format("Device passkey: {} {}", device.name(), passkey)); + } + + void agent_request_confirmation(bt::Device& device, uint32_t passkey, + std::function callback) override { + logger_.info(std::format("Device request confirmation: {} {}", + device.name(), passkey)); + callback(false); + } + + void agent_request_authorization( + bt::Device& device, std::function callback) override { + logger_.info( + std::format("Device request authorization: {}", device.name())); + callback(false); + } + + void agent_authorize_service(bt::Device& device, std::string const& uuid, + std::function callback) override { + logger_.info(std::format("Device request authorize service: {} {}", + device.name(), uuid)); + callback(false); + } + + void agent_cancel() override {} + + private: + logger::Logger& logger_; +}; + bool run(logger::Logger& logger, cfg::Config const& cfg, std::unique_ptr port) { auto looper = looper::create(); - HttpServerDelegate delegate; + HttpServerDelegate http_delegate; auto server = - http::create_server(logger, cfg, *looper, std::move(port), delegate); + http::create_server(logger, cfg, *looper, std::move(port), http_delegate); + BluetoothManagerDelegate bt_delegate(logger); + auto manager = bt::create_manager(logger, cfg, *looper, bt_delegate); + auto sigint_handler = signals::Handler::create( + *looper, signals::Signal::INT, [&looper, &logger]() { + logger.info("Received SIGINT, quitting..."); + looper->quit(); + }); + auto sigterm_handler = signals::Handler::create( + *looper, signals::Signal::TERM, [&looper, &logger]() { + logger.info("Received SIGTERM, quitting..."); + looper->quit(); + }); return looper->run(logger); } diff --git a/src/signals.cc b/src/signals.cc new file mode 100644 index 0000000..9f8bff7 --- /dev/null +++ b/src/signals.cc @@ -0,0 +1,89 @@ +#include "signals.hh" + +#include "looper.hh" +#include "unique_pipe.hh" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace signals { + +namespace { + +std::unordered_map g_fds; + +int signum(Signal signal) { + switch (signal) { + case Signal::INT: + return SIGINT; + case Signal::TERM: + return SIGTERM; + } + std::unreachable(); +} + +void signal_handler(int signum) { + auto it = g_fds.find(signum); + if (it != g_fds.end()) { + char c = 1; + while (true) { + auto ret = write(it->second, &c, 1); + if (ret < 0 && errno == EINTR) + continue; + break; + } + } +} + +class HandlerImpl : public Handler { + public: + HandlerImpl(looper::Looper& looper, Signal signal, + std::function callback) + : looper_(looper), + signal_(signum(signal)), + callback_(std::move(callback)) { + looper_.add(pipe_.reader(), looper::EVENT_READ, + [this](auto event) { call(event); }); + g_fds[signal_] = pipe_.writer(); + + ::signal(signal_, signal_handler); + } + + ~HandlerImpl() override { + ::signal(signal_, SIG_DFL); + looper_.remove(pipe_.reader()); + g_fds.erase(signal_); + } + + private: + void call(uint8_t /* event */) { + char buf[10]; + while (true) { + auto ret = read(pipe_.reader(), buf, 10); + if (ret < 0 && errno == EINTR) + continue; + break; + } + callback_(); + } + + unique_pipe pipe_; + looper::Looper& looper_; + int signal_; + std::function callback_; +}; + +} // namespace + +std::unique_ptr Handler::create(looper::Looper& looper, Signal signal, + std::function callback) { + return std::make_unique(looper, signal, std::move(callback)); +} + +} // namespace signals diff --git a/src/signals.hh b/src/signals.hh new file mode 100644 index 0000000..f387f9f --- /dev/null +++ b/src/signals.hh @@ -0,0 +1,33 @@ +#ifndef SIGNALS_HH +#define SIGNALS_HH + +#include +#include + +namespace looper { +class Looper; +} // namespace looper + +namespace signals { + +enum class Signal { + INT, + TERM, +}; + +class Handler { + public: + virtual ~Handler() = default; + + static std::unique_ptr create(looper::Looper& looper, Signal signal, + std::function callback); + + protected: + Handler() = default; + Handler(Handler const&) = delete; + Handler& operator=(Handler const&) = delete; +}; + +} // namespace signals + +#endif // SIGNALS_HH diff --git a/src/unique_pipe.cc b/src/unique_pipe.cc new file mode 100644 index 0000000..28c106d --- /dev/null +++ b/src/unique_pipe.cc @@ -0,0 +1,32 @@ +#include "unique_pipe.hh" + +#include +#include + +unique_pipe::unique_pipe() { + int fd[2]; + if (pipe(fd)) + return; + fd_[0] = unique_fd(fd[0]); + fd_[1] = unique_fd(fd[1]); +} + +unique_pipe::unique_pipe(unique_pipe&& fd) { + fd_[0] = unique_fd(fd.fd_[0].release()); + fd_[1] = unique_fd(fd.fd_[1].release()); +} + +unique_pipe& unique_pipe::operator=(unique_pipe&& fd) { + fd_[0].reset(fd.fd_[0].release()); + fd_[1].reset(fd.fd_[1].release()); + return *this; +} + +void unique_pipe::reset() { + fd_[0].reset(); + fd_[1].reset(); +} + +unique_fd unique_pipe::release_reader() { return std::move(fd_[0]); } + +unique_fd unique_pipe::release_writer() { return std::move(fd_[1]); } diff --git a/src/unique_pipe.hh b/src/unique_pipe.hh new file mode 100644 index 0000000..c133018 --- /dev/null +++ b/src/unique_pipe.hh @@ -0,0 +1,42 @@ +#ifndef UNIQUE_PIPE_HH +#define UNIQUE_PIPE_HH + +#include "unique_fd.hh" + +#include + +class unique_pipe { + public: + unique_pipe(); + unique_pipe(unique_pipe&& fd); + unique_pipe(unique_pipe const&) = delete; + unique_pipe& operator=(unique_pipe const&) = delete; + + ~unique_pipe() = default; + + unique_pipe& operator=(unique_pipe&& fd); + + [[nodiscard]] + int reader() const { + return fd_[0].get(); + } + [[nodiscard]] + int writer() const { + return fd_[1].get(); + } + + [[nodiscard]] + explicit operator bool() const { + return fd_[0] || fd_[1]; + } + + void reset(); + + unique_fd release_reader(); + unique_fd release_writer(); + + private: + unique_fd fd_[2]; +}; + +#endif // UNIQUE_PIPE_HH -- cgit v1.2.3-70-g09d2