This commit is contained in:
2025-05-13 01:34:53 +03:00
parent 427735e23d
commit 83f3f1c7d4
945 changed files with 633484 additions and 0 deletions
@@ -0,0 +1,204 @@
#include "epoll_event_loop.hpp"
#include "../logging.hpp"
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <string.h>
using namespace fibre;
DEFINE_LOG_TOPIC(EVENT_LOOP);
USE_LOG_TOPIC(EVENT_LOOP);
bool EpollEventLoop::start(Callback<void> on_started) {
if (epoll_fd_ >= 0) {
FIBRE_LOG(E) << "already started";
return false;
}
epoll_fd_ = epoll_create1(0);
if (epoll_fd_ < 0) {
FIBRE_LOG(E) << "epoll_create1() failed";
return false;
}
bool ok = true;
post_fd_ = eventfd(0, 0);
bool post_fd_ok = (post_fd_ >= 0)
&& register_event(post_fd_, EPOLLIN, MEMBER_CB(this, run_callbacks))
&& post(on_started);
if (!post_fd_ok) {
FIBRE_LOG(E) << "failed to create an event for posting callbacks onto the event loop";
ok = false;
}
// Run for as long as there are callbacks pending posted or there's at least
// one file descriptor other than post_fd_ registerd.
while (pending_callbacks_.size() || (context_map_.size() > 1)) {
iterations_++;
do {
FIBRE_LOG(D) << "epoll_wait...";
n_triggered_events_ = epoll_wait(epoll_fd_, triggered_events_, max_triggered_events_, -1);
FIBRE_LOG(D) << "epoll_wait unblocked by " << n_triggered_events_ << " events";
if (errno == EINTR) {
FIBRE_LOG(D) << "interrupted";
}
} while (n_triggered_events_ < 0 && errno == EINTR); // ignore syscall interruptions. This happens for instance during suspend.
if (n_triggered_events_ <= 0) {
FIBRE_LOG(E) << "epoll_wait() failed with " << n_triggered_events_ << ": " << sys_err() << " - Terminating worker thread.";
ok = false;
break;
}
// Handle events
for (int i = 0; i < n_triggered_events_; ++i) {
EventContext* ctx = (EventContext*)triggered_events_[i].data.ptr;
if (ctx) {
try { // TODO: not sure if using "try" without throwing exceptions will do unwanted things with the stack
ctx->callback.invoke(triggered_events_[i].events);
} catch (...) {
FIBRE_LOG(E) << "worker callback threw an exception.";
}
}
}
}
FIBRE_LOG(D) << "epoll loop exited";
if ((post_fd_ >= 0) && !deregister_event(post_fd_)) {
FIBRE_LOG(E) << "deregister_event() failed";
ok = false;
}
if ((post_fd_ >= 0) && close(post_fd_) != 0) {
FIBRE_LOG(E) << "close() failed: " << sys_err();
ok = false;
}
post_fd_ = -1;
if (close(epoll_fd_) != 0) {
FIBRE_LOG(E) << "close() failed: " << sys_err();
ok = false;
}
epoll_fd_ = -1;
return ok;
}
bool EpollEventLoop::post(Callback<void> callback) {
if (epoll_fd_ < 0) {
FIBRE_LOG(E) << "not started";
return false;
}
{
std::unique_lock<std::mutex> lock(pending_callbacks_mutex_);
pending_callbacks_.push_back(callback);
}
const uint64_t val = 1;
if (write(post_fd_, &val, sizeof(val)) != sizeof(val)) {
FIBRE_LOG(E) << "write() failed" << sys_err();
return false;
}
return true;
}
bool EpollEventLoop::register_event(int event_fd, uint32_t events, Callback<void, uint32_t> callback) {
if (epoll_fd_ < 0) {
FIBRE_LOG(E) << "not initialized";
return false;
}
if (event_fd < 0) {
FIBRE_LOG(E) << "invalid argument";
return false;
}
EventContext* ctx = new EventContext{callback};
struct epoll_event ev = {
.events = events,
.data = { .ptr = ctx }
};
context_map_[event_fd] = ctx;
if (epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, event_fd, &ev) != 0) {
FIBRE_LOG(E) << "epoll_ctl(" << event_fd << "...) failed: " << sys_err();
delete ctx;
return false;
}
FIBRE_LOG(D) << "registered epoll event " << event_fd;
return true;
}
bool EpollEventLoop::deregister_event(int event_fd) {
if (epoll_fd_ < 0) {
FIBRE_LOG(E) << "not running";
return false;
}
int result = true;
if (epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, event_fd, nullptr) != 0) {
FIBRE_LOG(E) << "epoll_ctl() failed: " << sys_err();
result = false;
}
EventContext* callback = context_map_[event_fd];
auto it = context_map_.find(event_fd);
if (it == context_map_.end()) {
FIBRE_LOG(E) << "event context not found";
return false;
}
for (int i = 0; i < n_triggered_events_; ++i) {
if ((EventContext*)(triggered_events_[i].data.ptr) == it->second) {
triggered_events_[i].data.ptr = nullptr;
}
}
context_map_.erase(it);
return result;
}
struct EventLoopTimer* EpollEventLoop::call_later(float delay, Callback<void> callback) {
FIBRE_LOG(E) << "not implemented"; // TODO: implement
return nullptr;
}
bool EpollEventLoop::cancel_timer(EventLoopTimer* timer) {
FIBRE_LOG(E) << "not implemented"; // TODO: implement
return false;
}
void EpollEventLoop::run_callbacks(uint32_t) {
// TODO: warn if read fails
uint64_t val;
if (read(post_fd_, &val, sizeof(val)) != sizeof(val)) {
FIBRE_LOG(E) << "failed to read from post file descriptor";
}
std::vector<Callback<void>> pending_callbacks;
{
std::unique_lock<std::mutex> lock(pending_callbacks_mutex_);
std::swap(pending_callbacks, pending_callbacks_);
}
for (auto& cb: pending_callbacks) {
cb.invoke();
}
}
@@ -0,0 +1,69 @@
#ifndef __FIBRE_LINUX_EVENT_LOOP_HPP
#define __FIBRE_LINUX_EVENT_LOOP_HPP
//#include <thread>
#include <sys/epoll.h>
#include <unordered_map>
#include <vector>
#include <mutex>
//#include <algorithm>
#include <fibre/event_loop.hpp>
namespace fibre {
/**
* @brief Event loop based on the Linux-specific `epoll()` infrastructure.
*
* Thread safety: None of the public functions are thread-safe with respect to
* each other. However they are thread safe with respect to the internal event
* loop, that means register_event() and deregister_event() can be called from
* within an event callback (which executes on the event loop thread), provided
* those calls are properly synchronized with calls from other threads.
*/
class EpollEventLoop : public EventLoop {
public:
/**
* @brief Starts the event loop on the current thread and places the
* specified start callback on the event queue.
*
* The function returns when the event loop becomes empty or if a platform
* error occurs.
*/
bool start(Callback<void> on_started);
bool post(Callback<void> callback) final;
bool register_event(int fd, uint32_t events, Callback<void, uint32_t> callback) final;
bool deregister_event(int fd) final;
struct EventLoopTimer* call_later(float delay, Callback<void> callback) final;
bool cancel_timer(EventLoopTimer* timer) final;
private:
struct EventContext {
//int fd;
Callback<void, uint32_t> callback;
};
void run_callbacks(uint32_t);
int epoll_fd_ = -1;
int post_fd_ = -1;
unsigned int iterations_ = 0;
std::unordered_map<int, EventContext*> context_map_; // required to deregister callbacks
static const size_t max_triggered_events_ = 16; // max number of events that can be handled per iteration
int n_triggered_events_ = 0;
struct epoll_event triggered_events_[max_triggered_events_];
// List of callbacks that were submitted through post().
std::vector<Callback<void>> pending_callbacks_;
// Mutex to protect pending_callbacks_
std::mutex pending_callbacks_mutex_;
};
}
#endif // __FIBRE_LINUX_EVENT_LOOP_HPP
@@ -0,0 +1,684 @@
/**
* @brief Transport provider: libusb
*
* Platform Compatibility: Linux, Windows, macOS
*/
#include "libusb_transport.hpp"
#include "../logging.hpp"
#include "../print_utils.hpp"
#include <fibre/fibre.hpp>
#include <algorithm>
#include <string.h>
#if !FIBRE_ALLOW_HEAP
# error "The libusb backend requires heap allocation."
#endif
using namespace fibre;
DEFINE_LOG_TOPIC(USB);
USE_LOG_TOPIC(USB);
// This probably has no noteworthy effect since we automatically restart
// timed out operations anyway.
constexpr unsigned int kBulkTimeoutMs = 10000;
// Only relevant for platforms don't support hotplug detection and thus
// need polling.
constexpr unsigned int kPollingIntervalMs = 1000;
/* LibusbDiscoverer ----------------------------------------------------------*/
/**
* @brief Initializes the discoverer.
*
* Asynchronous tasks will be executed on the provided event_loop.
*
* @param event_loop: The event loop that is used to execute background tasks. The
* pointer must be non-null and initialized when this function is called.
* It must remain initialized until deinit() of this discoverer was called.
*/
bool LibusbDiscoverer::init(EventLoop* event_loop) {
if (!event_loop)
return false;
event_loop_ = event_loop;
if (libusb_init(&libusb_ctx_) != LIBUSB_SUCCESS) {
FIBRE_LOG(E) << "libusb_init() failed: " << sys_err();
return deinit(0), false;
}
// Fetch initial list of file-descriptors we have to monitor.
// Note: this will fail on Windows. Since this is used for epoll, we need a
// different approach for Windows anyway.
const struct libusb_pollfd** pollfds = libusb_get_pollfds(libusb_ctx_);
using_sparate_libusb_thread_ = !pollfds;
if (!using_sparate_libusb_thread_) {
// This code path is taken on Linux
FIBRE_LOG(D) << "Using externally provided event loop";
// Check if libusb needs special time-based polling on this platform
if (libusb_pollfds_handle_timeouts(libusb_ctx_) == 0) {
FIBRE_LOG(D) << "Using time-based polling";
}
// libusb maintains a (dynamic) list of file descriptors that need to be
// monitored (via select/poll/epoll) so that I/O events can be processed when
// needed. Since we use the async libusb interface, we do the monitoring
// ourselves. That means we always need keep track of the libusb file
// descriptor list.
// Subscribe to changes to the list of file-descriptors we have to monitor.
libusb_set_pollfd_notifiers(libusb_ctx_,
[](int fd, short events, void *user_data) {
((LibusbDiscoverer*)user_data)->on_add_pollfd(fd, events);
},
[](int fd, void *user_data) {
((LibusbDiscoverer*)user_data)->on_remove_pollfd(fd);
}, this);
// Fetch initial list of file-descriptors we have to monitor.
// Note: this will fail on Windows. Since this is used for epoll, we need a
// different approach for Windows anyway.
const struct libusb_pollfd** pollfds = libusb_get_pollfds(libusb_ctx_);
if (!pollfds) {
return deinit(2), false;
}
for (size_t i = 0; pollfds[i]; ++i) {
on_add_pollfd(pollfds[i]->fd, pollfds[i]->events);
}
libusb_free_pollfds(pollfds);
pollfds = nullptr;
} else {
FIBRE_LOG(D) << "Using internal event loop thread";
// This code path is taken on Windows (which does not support epoll)
run_internal_event_loop_ = true;
internal_event_loop_thread_ = new std::thread([](void* ctx) {
((LibusbDiscoverer*)ctx)->internal_event_loop();
}, this);
}
if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
// This code path is taken on Linux
FIBRE_LOG(D) << "Using libusb native hotplug detection";
// Subscribe to hotplug events
int result = libusb_hotplug_register_callback(libusb_ctx_,
(libusb_hotplug_event)(LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT),
LIBUSB_HOTPLUG_ENUMERATE /* trigger callback for all currently connected devices too */,
LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY,
[](struct libusb_context *ctx, struct libusb_device *dev, libusb_hotplug_event event, void *user_data){
return ((LibusbDiscoverer*)user_data)->on_hotplug(dev, event);
}, this, &hotplug_callback_handle_);
if (LIBUSB_SUCCESS != result) {
FIBRE_LOG(E) << "Error subscribing to hotplug events";
hotplug_callback_handle_ = 0;
return deinit(3), false;
}
} else {
// This code path is taken on Windows
FIBRE_LOG(D) << "Using periodic polling to discover devices";
poll_devices_now(); // this will also start a timer to poll again periodically
}
if (!pollfds && libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
// The hotplug callback handler above is not yet thread-safe. To make it thread-safe
// we'd need to post it on the application's event loop.
FIBRE_LOG(E) << "Hotplug detection with separate libusb thread will cause trouble.";
}
return true;
}
bool LibusbDiscoverer::deinit(int stage) {
// TODO: verify that all devices are closed and hotplug detection is disabled
if (stage > 3 && libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
libusb_hotplug_deregister_callback(libusb_ctx_, hotplug_callback_handle_);
}
if (stage > 3 && device_polling_timer_) {
event_loop_->cancel_timer(device_polling_timer_);
device_polling_timer_ = nullptr;
}
if (stage > 2 && !run_internal_event_loop_) {
// Deregister libusb events from our event loop.
const struct libusb_pollfd** pollfds = libusb_get_pollfds(libusb_ctx_);
if (pollfds) {
for (size_t i = 0; pollfds[i]; ++i) {
on_remove_pollfd(pollfds[i]->fd);
}
libusb_free_pollfds(pollfds);
pollfds = nullptr;
}
}
if (stage > 1 && !run_internal_event_loop_) {
libusb_set_pollfd_notifiers(libusb_ctx_, nullptr, nullptr, nullptr);
}
if (stage > 0 && run_internal_event_loop_) {
run_internal_event_loop_ = false;
libusb_interrupt_event_handler(libusb_ctx_);
internal_event_loop_thread_->join();
delete internal_event_loop_thread_;
internal_event_loop_thread_ = nullptr;
}
if (stage > 0) {
// TODO: we should probably deinit and close all connected channels
for (auto& dev: known_devices_) {
libusb_unref_device(dev.second.dev);
}
}
// FIXME: the libusb_hotplug_deregister_callback call will still trigger a
// usb_handler event. We need to wait until this has finished before we
// truly discard libusb resources
// Update: is this still relevant?
//usleep(100000);
if (stage > 0) {
libusb_exit(libusb_ctx_);
libusb_ctx_ = nullptr;
}
event_loop_ = nullptr;
return true;
}
/**
* @brief Starts looking for Fibre devices accessible through USB.
*
* Multiple discovery requests can be active at the same time but beware that a
* channel will be announced to all matching subscribers so be careful with access
* multiplexing.
*
* If the function succeeds, an opaque context pointer is returned which must be
* passed to stop_channel_discovery() to terminate this particular request.
*
* @param specs: See README of the main Fibre repository for details.
* (https://github.com/samuelsadok/fibre/tree/devel).
*
* @param on_found_channels: Invoked when a matching pair of RX/TX channels is found.
* This callback will also be called for any matching channels that already exist when
* the discovery is started.
*/
void LibusbDiscoverer::start_channel_discovery(Domain* domain, const char* specs, size_t specs_len, ChannelDiscoveryContext** handle) {
FIBRE_LOG(D) << "starting discovery with filter \"" << std::string(specs, specs_len) << "\"";
InterfaceSpecs interface_specs;
try_parse_key(specs, specs + specs_len, "bus", &interface_specs.bus);
try_parse_key(specs, specs + specs_len, "address", &interface_specs.address);
try_parse_key(specs, specs + specs_len, "idVendor", &interface_specs.vendor_id);
try_parse_key(specs, specs + specs_len, "idProduct", &interface_specs.product_id);
try_parse_key(specs, specs + specs_len, "bInterfaceClass", &interface_specs.interface_class);
try_parse_key(specs, specs + specs_len, "bInterfaceSubClass", &interface_specs.interface_subclass);
try_parse_key(specs, specs + specs_len, "bInterfaceProtocol", &interface_specs.interface_protocol);
MyChannelDiscoveryContext* subscription = new MyChannelDiscoveryContext{};
subscription->interface_specs = interface_specs;
subscription->domain = domain;
subscriptions_.push_back(subscription);
for (auto& dev: known_devices_) {
consider_device(dev.second.dev, subscription);
}
if (handle) {
*handle = subscription;
}
return;
}
/**
* @brief Stops an object discovery process that was started with start_channel_discovery().
*
* Channels which were already discovered will remain open. However if the discovery is restarted
* it is possible that the same channels are returned again (their pointers need not match the old instance).
*
* The discovery must be considered still in progress until the callback is
* invoked with kFibreCancelled.
*/
int LibusbDiscoverer::stop_channel_discovery(ChannelDiscoveryContext* handle) {
auto it = std::find(subscriptions_.begin(), subscriptions_.end(), handle);
if (it == subscriptions_.end()) {
FIBRE_LOG(E) << "Not an active subscription";
return -1;
}
subscriptions_.erase(it);
delete handle;
return 0;
}
/**
* @brief Runs the event handling loop. This function blocks until
* run_internal_event_loop_ is false.
*
* This loop is only executed on Windows. On other platforms the provided EventLoop is used.
*/
void LibusbDiscoverer::internal_event_loop() {
while (run_internal_event_loop_)
libusb_handle_events(libusb_ctx_);
}
void LibusbDiscoverer::on_event_loop_iteration() {
if (event_loop_timer_) {
FIBRE_LOG(D) << "cancelling event loop timer";
event_loop_->cancel_timer(event_loop_timer_);
event_loop_timer_ = nullptr;
}
timeval tv = { .tv_sec = 0, .tv_usec = 0 };
if (libusb_handle_events_timeout(libusb_ctx_, &tv) != 0) {
FIBRE_LOG(E) << "libusb_handle_events_timeout() failed";
}
timeval timeout;
if (libusb_get_next_timeout(libusb_ctx_, &timeout)) {
float timeout_sec = (float)timeout.tv_sec + (float)timeout.tv_usec * 1e-6;
FIBRE_LOG(D) << "setting event loop timeout to " << timeout_sec << " s";
event_loop_timer_ = event_loop_->call_later(timeout_sec,
MEMBER_CB(this, on_event_loop_iteration));
}
}
/**
* @brief Called when libusb wants to add a file descriptor to our event loop.
*/
void LibusbDiscoverer::on_add_pollfd(int fd, short events) {
event_loop_->register_event(fd, events,
MEMBER_CB(this, on_event_loop_iteration2));
}
/**
* @brief Called when libusb wants to remove a file descriptor to our event loop.
*/
void LibusbDiscoverer::on_remove_pollfd(int fd) {
event_loop_->deregister_event(fd);
}
/**
* @brief Called by libusb when a USB device was plugged in or out.
*
* If this function returns a non-zero value, libusb removes this filter.
*/
int LibusbDiscoverer::on_hotplug(struct libusb_device *dev,
libusb_hotplug_event event) {
uint8_t bus_number = libusb_get_bus_number(dev);
uint8_t dev_number = libusb_get_device_address(dev);
if (LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED == event) {
FIBRE_LOG(D) << "device arrived: bus " << (int)bus_number << ", " << (int)dev_number;
// add empty placeholder to the list of known devices
known_devices_[bus_number << 8 | dev_number] = {
.dev = libusb_ref_device(dev),
.handle = nullptr
};
for (auto& subscription: subscriptions_) {
consider_device(dev, subscription);
}
} else if (LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT == event) {
FIBRE_LOG(D) << "device left: bus " << (int)bus_number << ", " << (int)dev_number;
auto it = known_devices_.find(bus_number << 8 | dev_number);
if (it != known_devices_.end()) {
for (auto& ep: it->second.ep_in) {
ep->deinit();
}
for (auto& ep: it->second.ep_out) {
ep->deinit();
}
if (it->second.handle) {
libusb_close(it->second.handle);
}
known_devices_.erase(it);
}
libusb_unref_device(dev);
} else {
FIBRE_LOG(W) << "Unexpected event: " << event;
}
return 0;
}
void LibusbDiscoverer::poll_devices_now() {
FIBRE_LOG(D) << "poll_devices_now() called.";
device_polling_timer_ = nullptr;
libusb_device** list = nullptr;
ssize_t n_devices = libusb_get_device_list(libusb_ctx_, &list);
std::unordered_map<uint16_t, libusb_device*> current_devices;
if (n_devices < 0) {
FIBRE_LOG(W) << "libusb_get_device_list() failed.";
} else {
for (ssize_t i = 0; i < n_devices; ++i) {
uint8_t bus_number = libusb_get_bus_number(list[i]);
uint8_t dev_number = libusb_get_device_address(list[i]);
current_devices[bus_number << 8 | dev_number] = list[i];
}
// Call on_hotplug for all new devices
for (auto& dev: current_devices) {
if (known_devices_.find(dev.first) == known_devices_.end()) {
on_hotplug(dev.second, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED);
// Immediately forget about the devices that weren't opened on plugin.
// The reason is this: On Windows the device address and even the
// device pointer can remain equal across device reset. Since we don't
// poll at infinite frequency This means we could miss a device reset.
// To avoid this, we reinspect the all unopened devices on
// every polling iteration.
auto it = known_devices_.find(dev.first);
if (it->second.handle == nullptr) {
known_devices_.erase(it);
}
}
}
// Call on_hotplug for all lost devices
std::vector<libusb_device*> lost_devices;
for (auto& dev: known_devices_) {
if (current_devices.find(dev.first) == current_devices.end()) {
lost_devices.push_back(dev.second.dev);
}
}
for (auto& dev: lost_devices) {
on_hotplug(dev, LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT);
}
libusb_free_device_list(list, 1 /* unref the devices */);
}
// It's possible that the discoverer was deinited during this function.
if (event_loop_) {
device_polling_timer_ = event_loop_->call_later(kPollingIntervalMs * 0.001f,
MEMBER_CB(this, poll_devices_now));
}
}
void LibusbDiscoverer::consider_device(struct libusb_device *device, MyChannelDiscoveryContext* subscription) {
uint8_t bus_number = libusb_get_bus_number(device);
uint8_t dev_number = libusb_get_device_address(device);
bool mismatch = (subscription->interface_specs.bus != -1 && bus_number != subscription->interface_specs.bus)
|| (subscription->interface_specs.address != -1 && dev_number != subscription->interface_specs.address);
if (mismatch) {
return;
}
if (subscription->interface_specs.vendor_id != -1 || subscription->interface_specs.product_id != -1) {
struct libusb_device_descriptor dev_desc;
int result = libusb_get_device_descriptor(device, &dev_desc);
if (result != LIBUSB_SUCCESS) {
FIBRE_LOG(W) << "Failed to get device descriptor: " << result;
}
mismatch = (subscription->interface_specs.vendor_id != -1 && dev_desc.idVendor != subscription->interface_specs.vendor_id)
|| (subscription->interface_specs.product_id != -1 && dev_desc.idProduct != subscription->interface_specs.product_id);
if (mismatch) {
return;
}
}
struct libusb_config_descriptor* config_desc = nullptr;
if (libusb_get_active_config_descriptor(device, &config_desc) != LIBUSB_SUCCESS) {
FIBRE_LOG(E) << "Failed to get active config descriptor: " << sys_err();
} else {
for (uint8_t i = 0; i < config_desc->bNumInterfaces; ++i) {
for (int j = 0; j < config_desc->interface[i].num_altsetting; ++j) {
// TODO: probably we should only chose one alt setting
const struct libusb_interface_descriptor* intf_desc = &(config_desc->interface[i].altsetting[j]);
mismatch = (subscription->interface_specs.interface_class != -1 && intf_desc->bInterfaceClass != subscription->interface_specs.interface_class)
|| (subscription->interface_specs.interface_subclass != -1 && intf_desc->bInterfaceSubClass != subscription->interface_specs.interface_subclass)
|| (subscription->interface_specs.interface_protocol != -1 && intf_desc->bInterfaceProtocol != subscription->interface_specs.interface_protocol);
if (mismatch) {
continue;
}
// We found a matching interface. Now find one bulk IN and one bulk OUT endpoint.
const libusb_endpoint_descriptor* libusb_ep_in = nullptr;
const libusb_endpoint_descriptor* libusb_ep_out = nullptr;
for (uint8_t k = 0; k < intf_desc->bNumEndpoints; ++k) {
if ((intf_desc->endpoint[k].bmAttributes & 0x03) == LIBUSB_TRANSFER_TYPE_BULK
&& (intf_desc->endpoint[k].bEndpointAddress & 0x80) == LIBUSB_ENDPOINT_IN) {
libusb_ep_in = &intf_desc->endpoint[k];
} else if ((intf_desc->endpoint[k].bmAttributes & 0x03) == LIBUSB_TRANSFER_TYPE_BULK
&& (intf_desc->endpoint[k].bEndpointAddress & 0x80) == LIBUSB_ENDPOINT_OUT) {
libusb_ep_out = &intf_desc->endpoint[k];
}
}
Device& my_dev = known_devices_[bus_number << 8 | dev_number];
// If the same device was already returned in a previous discovery
// then it will already be open.
if (!my_dev.handle) {
int result = libusb_open(device, &my_dev.handle);
if (LIBUSB_SUCCESS != result) {
FIBRE_LOG(E) << "Could not open USB device: " << result;
continue;
}
}
int result = libusb_claim_interface(my_dev.handle, i);
if (LIBUSB_SUCCESS != result) {
FIBRE_LOG(E) << "Could not claim interface " << i << " on USB device: " << result;
continue;
}
size_t mtu = SIZE_MAX;
LibusbBulkInEndpoint* ep_in = new LibusbBulkInEndpoint();
if (libusb_ep_in && ep_in->init(this, my_dev.handle, libusb_ep_in->bEndpointAddress)) {
my_dev.ep_in.push_back(ep_in);
mtu = std::min(mtu, (size_t)libusb_ep_in->wMaxPacketSize);
} else {
delete ep_in;
ep_in = nullptr;
}
LibusbBulkOutEndpoint* ep_out = new LibusbBulkOutEndpoint();
if (libusb_ep_out && ep_out->init(this, my_dev.handle, libusb_ep_out->bEndpointAddress)) {
my_dev.ep_out.push_back(ep_out);
mtu = std::min(mtu, (size_t)libusb_ep_out->wMaxPacketSize);
} else {
delete ep_out;
ep_out = nullptr;
}
subscription->domain->add_channels({kFibreOk, ep_in, ep_out, mtu});
}
}
libusb_free_config_descriptor(config_desc);
config_desc = nullptr;
}
}
/* LibusbBulkEndpoint --------------------------------------------------------*/
template<typename TRes>
bool LibusbBulkEndpoint<TRes>::init(LibusbDiscoverer* parent, libusb_device_handle* handle, uint8_t endpoint_id) {
parent_ = parent;
handle_ = handle;
transfer_ = libusb_alloc_transfer(0);
endpoint_id_ = endpoint_id;
return true;
}
template<typename TRes>
bool LibusbBulkEndpoint<TRes>::deinit() {
if (completer_) {
FIBRE_LOG(E) << "Transfer on EP " << as_hex(endpoint_id_) << " still in progress. This is gonna be messy.";
}
libusb_free_transfer(transfer_);
transfer_ = nullptr;
return true;
}
template<typename TRes>
void LibusbBulkEndpoint<TRes>::start_transfer(bufptr_t buffer, TransferHandle* handle, Callback<void, TRes> completer) {
if (handle) {
*handle = reinterpret_cast<TransferHandle>(this);
}
if (completer_) {
FIBRE_LOG(E) << "transfer already in progress";
completer.invoke({kStreamError, nullptr});
return;
}
if (!handle_) {
FIBRE_LOG(E) << "device not open";
completer.invoke({kStreamError, nullptr});
return;
}
auto direct_callback = [](struct libusb_transfer* transfer){
((LibusbBulkEndpoint<TRes>*)transfer->user_data)->on_transfer_finished();
};
// This callback is used if we start our own libusb thread
// separate from the application's event loop thread
auto indirect_callback = [](struct libusb_transfer* transfer){
auto ep = (LibusbBulkEndpoint<TRes>*)transfer->user_data;
ep->parent_->event_loop_->post(MEMBER_CB(ep, on_transfer_finished));
};
//FIBRE_LOG(D) << "transfer of size " << buffer.size();
libusb_fill_bulk_transfer(transfer_, handle_, endpoint_id_,
buffer.begin(), buffer.size(),
parent_->using_sparate_libusb_thread_ ? indirect_callback : direct_callback,
this, kBulkTimeoutMs);
completer_ = completer;
submit_transfer();
}
template<typename TRes>
void LibusbBulkEndpoint<TRes>::cancel_transfer(TransferHandle transfer_handle) {
if (!completer_) {
FIBRE_LOG(E) << "transfer not in progress";
return;
}
libusb_cancel_transfer(transfer_);
}
template<typename TRes>
void LibusbBulkEndpoint<TRes>::submit_transfer() {
int result = libusb_submit_transfer(transfer_);
if (LIBUSB_SUCCESS == result) {
// ok
FIBRE_LOG(T) << "started USB transfer on EP " << as_hex(endpoint_id_);
} else if (LIBUSB_ERROR_NO_DEVICE == result) {
FIBRE_LOG(W) << "couldn't start USB transfer on EP " << as_hex(endpoint_id_) << ": " << libusb_error_name(result);
completer_.invoke_and_clear({kStreamClosed, nullptr});
} else {
FIBRE_LOG(W) << "couldn't start USB transfer on EP " << as_hex(endpoint_id_) << ": " << libusb_error_name(result);
completer_.invoke_and_clear({kStreamError, nullptr});
}
}
template<typename TRes>
void LibusbBulkEndpoint<TRes>::on_transfer_finished() {
// We ignore timeouts here and just retry. If the application wishes to have
// a timeout on the transfer it can just call cancel_transfer() after a while.
if (transfer_->status == LIBUSB_TRANSFER_TIMED_OUT) {
submit_transfer();
return;
}
libusb_device* dev = libusb_get_device(handle_);
StreamStatus status;
if (transfer_->status == LIBUSB_TRANSFER_COMPLETED) {
status = kStreamOk;
} else if (transfer_->status == LIBUSB_TRANSFER_CANCELLED) {
status = kStreamCancelled;
} else {
// The error that we get on device removal tends to be inaccurate.
// Sometimes it's LIBUSB_TRANSFER_STALL, sometimes
// LIBUSB_TRANSFER_ERROR. Therefore we just check if the device
// is still present to determine which error code to return.
// TODO: this detection doesn't really work. The device is still in the
// device list at this point when it just got unplugged. For now we
// just ignore transfer errors.
bool found = false;
libusb_device** list;
ssize_t n_devices = libusb_get_device_list(parent_->libusb_ctx_, &list);
if (n_devices >= 0) {
for (size_t i = 0; i < (size_t)n_devices; ++i) {
if (list[i] == dev) {
// found = true;
break;
}
}
libusb_free_device_list(list, 1);
}
if (found) {
status = kStreamError;
} else {
FIBRE_LOG(D) << "device removed during transfer";
status = kStreamClosed;
}
}
(status == kStreamError ? FIBRE_LOG(W) : FIBRE_LOG(T))
<< "USB transfer on EP " << as_hex(endpoint_id_) << " finished with " << libusb_error_name(transfer_->status);
if (status == kStreamClosed) {
handle_ = nullptr; // Ensure that no new transfer is started
}
uint8_t* end = std::max(transfer_->buffer + transfer_->actual_length, transfer_->buffer);
completer_.invoke_and_clear({status, end});
// If libusb does hotplug detection itself then we don't need to handle
// device removal here. Libusb will call the corresponding hotplug callback.
if (status == kStreamClosed && !parent_->hotplug_callback_handle_) {
if (!parent_->using_sparate_libusb_thread_) {
FIBRE_LOG(E) << "It's not a good idea to unref the device from within this callback. This will probably hang.";
}
parent_->on_hotplug(dev, LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT);
}
}
@@ -0,0 +1,125 @@
#ifndef __FIBRE_USB_DISCOVERER_HPP
#define __FIBRE_USB_DISCOVERER_HPP
#include <fibre/event_loop.hpp>
#include <fibre/async_stream.hpp>
#include <fibre/channel_discoverer.hpp>
#include <libusb.h>
#include <thread>
#include <vector>
#include <unordered_map>
namespace fibre {
class LibusbBulkInEndpoint;
class LibusbBulkOutEndpoint;
template<typename TRes> class LibusbBulkEndpoint;
class LibusbDiscoverer : public ChannelDiscoverer {
public:
struct InterfaceSpecs {
int bus = -1; // -1 to ignore
int address = -1; // -1 to ignore
int vendor_id = -1; // -1 to ignore
int product_id = -1; // -1 to ignore
int interface_class = -1; // -1 to ignore
int interface_subclass = -1; // -1 to ignore
int interface_protocol = -1; // -1 to ignore
};
struct MyChannelDiscoveryContext : ChannelDiscoveryContext {
InterfaceSpecs interface_specs;
Domain* domain;
};
constexpr static const char* get_name() { return "usb"; }
bool init(EventLoop* event_loop);
bool deinit() { return deinit(INT_MAX); }
void start_channel_discovery(Domain* domain, const char* specs, size_t specs_len, ChannelDiscoveryContext** handle) final;
int stop_channel_discovery(ChannelDiscoveryContext* handle) final;
private:
friend class LibusbBulkEndpoint<ReadResult>;
friend class LibusbBulkEndpoint<WriteResult>;
struct Device {
struct libusb_device* dev;
struct libusb_device_handle* handle;
std::vector<LibusbBulkInEndpoint*> ep_in;
std::vector<LibusbBulkOutEndpoint*> ep_out;
};
bool deinit(int stage);
void internal_event_loop();
void on_event_loop_iteration();
void on_event_loop_iteration2(uint32_t) { on_event_loop_iteration(); }
void on_add_pollfd(int fd, short events);
void on_remove_pollfd(int fd);
int on_hotplug(struct libusb_device *dev, libusb_hotplug_event event);
void poll_devices_now();
void consider_device(struct libusb_device *device, MyChannelDiscoveryContext* subscription);
EventLoop* event_loop_ = nullptr;
bool using_sparate_libusb_thread_; // true on Windows. Initialized in init()
libusb_context *libusb_ctx_ = nullptr; // libusb session
libusb_hotplug_callback_handle hotplug_callback_handle_ = 0;
bool run_internal_event_loop_ = false;
std::thread* internal_event_loop_thread_;
EventLoopTimer* device_polling_timer_;
EventLoopTimer* event_loop_timer_ = nullptr;
std::unordered_map<uint16_t, Device> known_devices_; // key: bus_number << 8 | dev_number
std::vector<MyChannelDiscoveryContext*> subscriptions_;
};
template<typename TRes>
class LibusbBulkEndpoint {
public:
bool init(LibusbDiscoverer* parent, struct libusb_device_handle* handle, uint8_t endpoint_id);
bool deinit();
protected:
void start_transfer(bufptr_t buffer, TransferHandle* handle, Callback<void, TRes> completer);
void cancel_transfer(TransferHandle transfer_handle);
private:
void submit_transfer();
void on_transfer_finished();
LibusbDiscoverer* parent_ = nullptr;
struct libusb_device_handle* handle_ = nullptr;
uint8_t endpoint_id_ = 0;
struct libusb_transfer* transfer_ = nullptr;
Callback<void, TRes> completer_ = nullptr;
};
class LibusbBulkInEndpoint final : public LibusbBulkEndpoint<ReadResult>, public AsyncStreamSource {
public:
void start_read(bufptr_t buffer, TransferHandle* handle, Callback<void, ReadResult> completer) final {
start_transfer(buffer, handle, completer);
}
void cancel_read(TransferHandle transfer_handle) final {
cancel_transfer(transfer_handle);
}
};
class LibusbBulkOutEndpoint final : public LibusbBulkEndpoint<WriteResult>, public AsyncStreamSink {
public:
void start_write(cbufptr_t buffer, TransferHandle* handle, Callback<void, WriteResult> completer) final {
start_transfer({
(unsigned char*)buffer.begin(),
buffer.size()
}, handle, completer);
}
void cancel_write(TransferHandle transfer_handle) final {
cancel_transfer(transfer_handle);
}
};
}
#endif // __FIBRE_USB_DISCOVERER_HPP
@@ -0,0 +1,505 @@
#include "posix_socket.hpp"
#include "../logging.hpp"
#include "../print_utils.hpp"
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
#include <sys/epoll.h>
DEFINE_LOG_TOPIC(SOCKET);
USE_LOG_TOPIC(SOCKET);
#define MAX_CONCURRENT_CONNECTIONS 128
using namespace fibre;
namespace fibre {
/**
* @brief Tag type to print the last socket error.
*
* This is very similar to sys_err(), except that on Windows it uses
* WSAGetLastError() instead of `errno` to fetch the last error code.
*/
struct sock_err {
sock_err() :
#if defined(_WIN32) || defined(_WIN64)
error_number(WSAGetLastError()) {}
#else
error_number(errno) {}
#endif
sock_err(int error_number) : error_number(error_number) {}
int error_number;
};
}
namespace std {
std::ostream& operator<<(std::ostream& stream, const struct sockaddr_storage& val) {
char buf[128];
if ((val.ss_family == AF_INET) && (inet_ntop(val.ss_family, ((struct sockaddr*)&val)->sa_data+2, buf, sizeof(buf)))) {
return stream << buf;
} else if ((val.ss_family == AF_INET6) && (inet_ntop(val.ss_family, ((struct sockaddr*)&val)->sa_data+6, buf, sizeof(buf)))) {
return stream << buf;
} else {
return stream << "(invalid address)";
}
}
std::ostream& operator<<(std::ostream& stream, const fibre::sock_err& err) {
return stream << strerror(err.error_number) << " (" << err.error_number << ")";
}
}
struct fibre::AddressResolutionContext {
struct addrinfo hints{};
std::string address_str;
std::string port_str;
EventLoop* event_loop;
Callback<void, std::optional<cbufptr_t>> callback;
int cmpl_fd;
struct gaicb gaicb{};
struct gaicb* list[1];
void on_gai_completed();
};
bool fibre::start_resolving_address(EventLoop* event_loop, std::tuple<std::string, int> address, bool passive, AddressResolutionContext** handle, Callback<void, std::optional<cbufptr_t>> callback) {
// deleted in on_gai_completed()
AddressResolutionContext* ctx = new AddressResolutionContext();
ctx->address_str = std::get<0>(address);
ctx->port_str = std::to_string(std::get<1>(address));
ctx->event_loop = event_loop;
ctx->callback = callback;
ctx->hints = {
.ai_flags = (passive ? AI_PASSIVE : 0),
.ai_family = AF_UNSPEC,
.ai_socktype = 0, // this makes apparently no difference for numerical addresses
};
ctx->gaicb = {
.ar_name = ctx->address_str.c_str(),
.ar_service = ctx->port_str.c_str(),
.ar_request = &ctx->hints
};
ctx->list[0] = &ctx->gaicb;
// An extra thread will be created once getaddrinfo_a() completes. This
// thread will post a callback onto the original event loop to do the actual
// handling of the result. This is of course exceedingly stupid but it's
// less bad than throwing around with actual signals that could hit threads
// that don't expect it.
struct sigevent sig = {
.sigev_value = { .sival_ptr = ctx },
//.sigev_signo = SIGRTMIN,
.sigev_notify = SIGEV_THREAD,
};
sig.sigev_notify_function = [](union sigval sigval) {
auto ctx = ((AddressResolutionContext*)sigval.sival_ptr);
ctx->event_loop->post(MEMBER_CB(ctx, on_gai_completed));
};
FIBRE_LOG(D) << "starting address resolution for " << ctx->address_str;
if (getaddrinfo_a(GAI_NOWAIT, ctx->list, 1, &sig) != 0) {
FIBRE_LOG(E) << "getaddrinfo_a() failed";
delete ctx;
return false;
}
return true;
}
void fibre::cancel_resolving_address(AddressResolutionContext* handle) {
gai_cancel(&handle->gaicb);
}
void AddressResolutionContext::on_gai_completed() {
FIBRE_LOG(D) << "address resolution complete";
if (gai_error(&gaicb) != 0) {
FIBRE_LOG(W) << "failed to resolve " << address_str << ": " << sys_err();
} else {
// this returns multiple addresses
for (struct addrinfo* addr = gaicb.ar_result; addr; addr = addr->ai_next) {
FIBRE_LOG(D) << "resolved IP: " << *(struct sockaddr_storage*)addr->ai_addr;
cbufptr_t buf = {(const uint8_t*)addr->ai_addr, (size_t)addr->ai_addrlen};
callback.invoke(buf);
}
}
freeaddrinfo(gaicb.ar_result);
callback.invoke(std::nullopt); // Announce completion of the request
delete this;
}
struct fibre::ConnectionContext {
EventLoop* event_loop;
socket_id_t socket_id;
Callback<void, std::optional<socket_id_t>> callback;
void on_connection_complete(uint32_t mask);
void on_accept(uint32_t mask);
};
bool fibre::start_connecting(EventLoop* event_loop, cbufptr_t addr, int type, int protocol, ConnectionContext** ctx, Callback<void, std::optional<socket_id_t>> on_connected) {
auto the_addr = reinterpret_cast<const struct sockaddr*>(addr.begin());
ConnectionContext* context = new ConnectionContext();
context->event_loop = event_loop;
context->socket_id = socket(the_addr->sa_family, type | SOCK_NONBLOCK, protocol);
context->callback = on_connected;
if (IS_INVALID_SOCKET(context->socket_id)) {
FIBRE_LOG(E) << "failed to open socket: " << sock_err();
goto fail0;
}
if (connect(context->socket_id, the_addr, addr.size()) == 0) {
if (errno != EINPROGRESS) {
FIBRE_LOG(E) << "connect() failed: " << sock_err();
goto fail1;
}
}
if (!event_loop->register_event(context->socket_id, EPOLLOUT, MEMBER_CB(context, on_connection_complete))) {
FIBRE_LOG(E) << "failed to register event: " << sock_err();
goto fail1;
}
if (ctx) {
*ctx = context;
}
return true;
fail1:
close(context->socket_id);
fail0:
delete context;
return false;
}
void fibre::stop_connecting(ConnectionContext* ctx) {
if (!ctx->event_loop->deregister_event(ctx->socket_id)) {
FIBRE_LOG(W) << "failed to deregister event";
}
if (close(ctx->socket_id) != 0) {
FIBRE_LOG(W) << "failed to close socket";
}
ctx->socket_id = INVALID_SOCKET;
ctx->callback.invoke_and_clear(std::nullopt);
delete ctx;
}
void fibre::ConnectionContext::on_connection_complete(uint32_t mask) {
bool failed;
int error_code;
socklen_t error_code_size = sizeof(error_code);
if (getsockopt(socket_id, SOL_SOCKET, SO_ERROR, &error_code, &error_code_size) != 0) {
FIBRE_LOG(W) << "connection failed (unknown error)";
failed = true;
} else if (error_code != 0) {
FIBRE_LOG(W) << "connection failed: " << sock_err{error_code};
failed = true;
} else {
failed = false;
}
event_loop->deregister_event(socket_id);
callback.invoke(failed ? std::nullopt : std::make_optional(socket_id));
close(socket_id); // The callback must duplicate the socket id if it intends
// to keep using it.
delete this;
}
bool fibre::start_listening(EventLoop* event_loop, cbufptr_t addr, int type, int protocol, ConnectionContext** ctx, Callback<void, std::optional<socket_id_t>> on_connected) {
auto the_addr = reinterpret_cast<const struct sockaddr*>(addr.begin());
int flag = 1;
ConnectionContext* context = new ConnectionContext();
context->event_loop = event_loop;
context->socket_id = socket(the_addr->sa_family, type | SOCK_NONBLOCK, protocol);
context->callback = on_connected;
if (IS_INVALID_SOCKET(context->socket_id)) {
FIBRE_LOG(E) << "failed to open socket: " << sock_err();
goto fail0;
}
// Reuse local address.
// This helps reusing ports that were previously not closed cleanly and
// are therefore still lingering in the TIME_WAIT state.
if (setsockopt(context->socket_id, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag))) {
FIBRE_LOG(E) << "failed to make socket reuse addresses: " << sock_err();
goto fail1;
}
if (bind(context->socket_id, the_addr, addr.size())) {
FIBRE_LOG(E) << "failed to bind socket: " << sock_err();
goto fail1;
}
// make this socket a passive socket
if (listen(context->socket_id, MAX_CONCURRENT_CONNECTIONS) != 0) {
FIBRE_LOG(E) << "failed to listen on TCP: " << sys_err();
goto fail1;
}
if (!event_loop->register_event(context->socket_id, EPOLLIN, MEMBER_CB(context, on_accept))) {
FIBRE_LOG(E) << "failed to register event: " << sock_err();
goto fail1;
}
return true;
fail1:
close(context->socket_id);
fail0:
delete context;
return false;
}
void fibre::stop_listening(ConnectionContext* ctx) {
stop_connecting(ctx); // same implementation
}
void fibre::ConnectionContext::on_accept(uint32_t mask) {
struct sockaddr_storage remote_addr;
socklen_t slen = sizeof(remote_addr);
FIBRE_LOG(D) << "incoming TCP connection";
int new_socket_id = accept(socket_id, reinterpret_cast<struct sockaddr *>(&remote_addr), &slen);
if (IS_INVALID_SOCKET(new_socket_id)) {
FIBRE_LOG(E) << "accept() returned invalid socket: " << sock_err();
return; // ignore and wait for next incoming connection
}
callback.invoke(std::make_optional(new_socket_id));
close(new_socket_id); // The callback must duplicate the socket id if it intends
// to keep using it.
}
bool PosixSocket::init(EventLoop* event_loop, socket_id_t socket_id) {
if (!IS_INVALID_SOCKET(socket_id_)) {
FIBRE_LOG(E) << "already initialized";
return false;
}
socket_id = dup(socket_id);
if (IS_INVALID_SOCKET(socket_id)) {
FIBRE_LOG(E) << "failed to duplicate socket: " << sock_err();
return false;
}
//if (!event_loop->register_event(socket_id, 0, MEMBER_CB(this, on_event))) {
// FIBRE_LOG(E) << "failed to register socket event";
// close(socket_id);
// return false;
//}
event_loop_ = event_loop;
socket_id_ = socket_id;
return true;
}
bool PosixSocket::deinit() {
if (IS_INVALID_SOCKET(socket_id_)) {
FIBRE_LOG(E) << "not initialized";
return false;
}
bool result = true;
if (::close(socket_id_)) {
FIBRE_LOG(E) << "close() failed: " << sock_err();
result = false;
}
socket_id_ = INVALID_SOCKET;
return result;
}
void PosixSocket::start_read(bufptr_t buffer, TransferHandle* handle, Callback<void, ReadResult> completer) {
if (rx_callback_) {
FIBRE_LOG(E) << "RX request already pending";
completer.invoke({kStreamError});
return;
}
if (handle) {
*handle = reinterpret_cast<TransferHandle>(this);
}
auto result = read_sync(buffer);
if (result.has_value()) {
completer.invoke(*result);
} else {
rx_buf_ = buffer;
rx_callback_ = completer;
update_subscription();
}
}
void PosixSocket::cancel_read(TransferHandle transfer_handle) {
if (transfer_handle != reinterpret_cast<TransferHandle>(this)) {
FIBRE_LOG(E) << "invalid handle";
} else if (!rx_callback_) {
FIBRE_LOG(E) << "no RX pending";
} else {
rx_callback_.invoke_and_clear({kStreamCancelled, rx_buf_.begin()});
}
}
void PosixSocket::start_write(cbufptr_t buffer, TransferHandle* handle, Callback<void, WriteResult> completer) {
if (tx_callback_) {
FIBRE_LOG(E) << "TX request already pending";
completer.invoke({kStreamError});
return;
}
if (handle) {
*handle = reinterpret_cast<TransferHandle>(this);
}
auto result = write_sync(buffer);
if (result.has_value()) {
completer.invoke(*result);
} else {
tx_buf_ = buffer;
tx_callback_ = completer;
update_subscription();
}
}
void PosixSocket::cancel_write(TransferHandle transfer_handle) {
if (transfer_handle != reinterpret_cast<TransferHandle>(this)) {
FIBRE_LOG(E) << "invalid handle";
} else if (!tx_callback_) {
FIBRE_LOG(E) << "no TX pending";
} else {
tx_callback_.invoke_and_clear({kStreamCancelled, tx_buf_.begin()});
}
}
std::optional<ReadResult> PosixSocket::read_sync(bufptr_t buffer) {
if (buffer.size() == 0) {
// Empty buffers mess with our socket-close detection
FIBRE_LOG(W) << "empty buffer not permitted";
}
socklen_t slen = sizeof(remote_addr_);
ssize_t n_received = recvfrom(socket_id_, buffer.begin(), buffer.size(),
MSG_DONTWAIT, reinterpret_cast<struct sockaddr *>(&remote_addr_), &slen);
if (n_received < 0) {
// If recvfrom returns -1 an errno is set to indicate the error.
auto err = sock_err{};
if (err.error_number == EAGAIN || err.error_number == EWOULDBLOCK) {
return std::nullopt;
} else {
FIBRE_LOG(E) << "Socket read failed: " << err;
return {{kStreamError, buffer.end()}}; // the function might have written to the buffer
}
} else if ((size_t)n_received > buffer.size()) {
FIBRE_LOG(E) << "received too many bytes";
return {{kStreamError, buffer.end()}};
} else if (n_received == 0) {
FIBRE_LOG(D) << "socket closed (RX half)";
return {{kStreamClosed, buffer.begin()}};
} else {
FIBRE_LOG(D) << "Received " << n_received << " bytes from " << remote_addr_;
return {{kStreamOk, buffer.begin() + n_received}};
}
}
std::optional<WriteResult> PosixSocket::write_sync(cbufptr_t buffer) {
if (buffer.size() == 0) {
// Empty buffers mess with our socket-close detection
FIBRE_LOG(W) << "empty buffer not permitted";
}
int n_sent = sendto(socket_id_, buffer.begin(), buffer.size(), MSG_DONTWAIT,
reinterpret_cast<struct sockaddr*>(&remote_addr_), sizeof(remote_addr_));
if (n_sent < 0) {
// If sendto returns -1 an errno is set to indicate the error.
auto err = sock_err{};
if (err.error_number == EAGAIN || err.error_number == EWOULDBLOCK) {
return std::nullopt;
} else {
FIBRE_LOG(E) << "Socket write failed: " << err;
return {{kStreamError, buffer.end()}}; // the function might have written to the buffer
}
} else if ((size_t)n_sent > buffer.size()) {
FIBRE_LOG(E) << "sent too many bytes";
return {{kStreamError, buffer.end()}};
} else if (n_sent == 0) {
FIBRE_LOG(D) << "socket closed (TX half)";
return {{kStreamClosed, buffer.begin()}};
} else {
FIBRE_LOG(D) << "Sent " << n_sent << " bytes to " << remote_addr_;
return {{kStreamOk, buffer.begin() + n_sent}};
}
}
void PosixSocket::update_subscription() {
uint32_t new_mask = (tx_callback_ ? EPOLLOUT : 0)
| (rx_callback_ ? EPOLLIN : 0);
if (new_mask != mask_) {
if (mask_) {
event_loop_->deregister_event(socket_id_);
}
mask_ = new_mask;
if (new_mask) {
event_loop_->register_event(socket_id_, new_mask, MEMBER_CB(this, on_event));
}
}
}
void PosixSocket::on_event(uint32_t mask) {
if (mask & EPOLLIN) {
// The socket is ready for RX. If an RX request is pending, handle it
// here, otherwise ignore the event.
if (rx_callback_) {
auto result = read_sync(rx_buf_);
rx_buf_ = {};
if (result.has_value()) {
rx_callback_.invoke_and_clear(*result);
}
}
}
if (mask & EPOLLOUT) {
// The socket is ready for RX. If an RX request is pending, handle it
// here, otherwise ignore the event.
if (tx_callback_) {
auto result = write_sync(tx_buf_);
tx_buf_ = {};
if (result.has_value()) {
tx_callback_.invoke_and_clear(*result);
}
}
}
if (mask & ~(EPOLLIN | EPOLLOUT)) {
FIBRE_LOG(E) << "unknown event mask: " << as_hex(mask);
}
update_subscription();
}
@@ -0,0 +1,173 @@
#ifndef __FIBRE_POSIX_SOCKET_HPP
#define __FIBRE_POSIX_SOCKET_HPP
#include <fibre/event_loop.hpp>
#include <netinet/in.h>
#include <string>
#include <fibre/cpp_utils.hpp>
#include <fibre/bufptr.hpp>
#include <fibre/async_stream.hpp>
namespace fibre {
#if defined(__linux__)
//using PosixSocketWorker = LinuxWorker; // TODO: rename to EPollWorker or LinuxEPollWorker
using socket_id_t = int;
#elif defined(_WIN32) || defined(_WIN64)
//using PosixSocketWorker = PosixPollWorker;
using socket_id_t = SOCKET;
#else
//using PosixSocketWorker = KQueueWorker;
using socket_id_t = int;
#endif
#if defined(_Win32) || defined(_Win64)
#define IS_INVALID_SOCKET(socket_id) (socket_id == INVALID_SOCKET)
#else
#define INVALID_SOCKET (-1)
#define IS_INVALID_SOCKET(socket_id) (socket_id < 0)
#endif
struct AddressResolutionContext;
struct ConnectionContext;
/**
* @brief Starts resolving a hostname (such as www.google.com) to one or
* multiple IP addresses.
*
* If available, both IPv4 and IPv6 addresses are returned.
*
* @param passive: If false, the returned address will be suitable for use with
* connect(2), sendto(2), or sendmsg(2).
* @param callback: Invoked for every address that is found. Invoked with null
* if no more addresses are available, including in case of an error or
* cancellation.
*
* @returns: false if the lookup could not be started. `callback` will not be
* called.
*/
bool start_resolving_address(EventLoop* event_loop,
std::tuple<std::string, int> address, bool passive,
AddressResolutionContext** handle,
Callback<void, std::optional<cbufptr_t>> callback);
/**
* @brief Cancels the ongoing address resolution.
*
* The cancellation is complete once the associated callback is invoked with
* null.
*/
void cancel_resolving_address(AddressResolutionContext* handle);
/**
* @brief Starts connecting to the specified address
*
* @param addr: The address to connect to. Usually this buffer contains an
* address of the type `struct sockaddr`. The family parameter of this
* address will be passed as 1st argument to socket().
* @param type: Will be passed as 2nd argument to socket(). Can be for
* instance SOCK_DGRAM or SOCKET_STREAM.
* @param protocol: Will be passed as 3rd argument to socket(). Can be for
* instance IPPROTO_UDP or IPPROTO_TCP.
* @param on_connected: Called when the connection attempt succeeds or fails.
* If the connection was established, the socket ID is passed to the
* callback. This socket ID will only be valid for the duration of the
* callback and must be duplicated (dup) if the application intends to
* keep using it.
* If the connection failed, std::nullopt is passed.
*/
bool start_connecting(EventLoop* event_loop, cbufptr_t addr, int type, int protocol, ConnectionContext** ctx, Callback<void, std::optional<socket_id_t>> on_connected);
void stop_connecting(ConnectionContext* ctx);
/**
* @brief Starts listening and accepting connections on the specified local
* address.
*
* @param addr: The local address to listen on. Usually this buffer contains an
* address of the type `struct sockaddr`. The family parameter of this
* address will be passed as 1st argument to socket().
* @param type: Will be passed as 2nd argument to socket(). Can be for
* instance SOCK_DGRAM or SOCKET_STREAM.
* @param protocol: Will be passed as 3rd argument to socket(). Can be for
* instance IPPROTO_UDP or IPPROTO_TCP.
* @param on_connected: Called for every connection that is accepted. The new
* socket ID is passed to the callback. This socket ID will only be valid
* for the duration of the callback and must be duplicated (dup) if the
* application intends to keep using it.
* If the attempt to listen fails permanently or is cancelled,
* std::nullopt is passed.
*/
bool start_listening(EventLoop* event_loop, cbufptr_t addr, int type, int protocol, ConnectionContext** ctx, Callback<void, std::optional<socket_id_t>> on_connected);
void stop_listening(ConnectionContext* ctx);
/**
* @brief AsyncStreamSource and AsyncStreamSink based on a Posix or WinSock
* socket ID.
*
* Note: To make this work on Windows, a "poll"-based worker must be implemented.
*/
class PosixSocket final : public AsyncStreamSource, public AsyncStreamSink {
public:
/**
* @brief Initializes the object with the given socket ID.
*
* The socket must be bound to a local address before this function is
* called.
*
* @param socket_id: For Unix-like systems this should be a file descriptor,
* for Windows this should be a Windows Socket ID (as returned by
* socket()).
* The socket must be in non-blocking mode (opened with O_NONBLOCK).
* The socket will internally be duplicated using dup() so it can be
* closed after this call.
*/
bool init(EventLoop* event_loop, socket_id_t socket_id);
/**
* @brief Deinits a socket that was initialized with init().
*/
bool deinit();
void start_read(bufptr_t buffer, TransferHandle* handle, Callback<void, ReadResult> completer) final;
void cancel_read(TransferHandle transfer_handle) final;
void start_write(cbufptr_t buffer, TransferHandle* handle, Callback<void, WriteResult> completer) final;
void cancel_write(TransferHandle transfer_handle) final;
/**
* @brief Returns the remote address of this socket.
*
* For connectionless sockets this is origin of the most recently received
* data and it is only valid from the moment something was actually received.
*
* For connection-oriented sockets this address is valid as soon as the
* socket is initialized.
*/
struct sockaddr_storage get_remote_address() const { return remote_addr_; }
private:
std::optional<ReadResult> read_sync(bufptr_t buffer);
std::optional<WriteResult> write_sync(cbufptr_t buffer);
void update_subscription();
void on_event(uint32_t mask);
int socket_id_ = INVALID_SOCKET;
EventLoop* event_loop_ = nullptr;
struct sockaddr_storage remote_addr_ = {0}; // updated after each RX event
uint32_t mask_ = 0; // current event subscription mask
bufptr_t rx_buf_{}; // valid while there is an RX request pending
cbufptr_t tx_buf_{}; // valid while there is a TX request pending
Callback<void, ReadResult> rx_callback_; // valid while there is an RX request pending
Callback<void, WriteResult> tx_callback_; // valid while there is a TX request pending
};
}
#include <iostream>
namespace std {
std::ostream& operator<<(std::ostream& stream, const struct sockaddr_storage& val);
}
#endif // __FIBRE_POSIX_SOCKET_HPP
@@ -0,0 +1,138 @@
#include "posix_tcp_backend.hpp"
#include "posix_socket.hpp"
#include "../logging.hpp"
#include <fibre/fibre.hpp>
#include <signal.h>
#include <unistd.h>
#include <algorithm>
#include <string.h>
DEFINE_LOG_TOPIC(TCP);
USE_LOG_TOPIC(TCP);
using namespace fibre;
bool PosixTcpBackend::init(EventLoop* event_loop) {
if (event_loop_) {
FIBRE_LOG(E) << "already initialized";
return false;
}
event_loop_ = event_loop;
return true;
}
bool PosixTcpBackend::deinit() {
if (!event_loop_) {
FIBRE_LOG(E) << "not initialized";
return false;
}
if (n_discoveries_) {
FIBRE_LOG(W) << "some discoveries still ongoing";
}
event_loop_ = nullptr;
return true;
}
void PosixTcpBackend::start_channel_discovery(Domain* domain, const char* specs, size_t specs_len, ChannelDiscoveryContext** handle) {
const char* address_begin;
const char* address_end;
int port;
if (!event_loop_) {
FIBRE_LOG(E) << "not initialized";
//on_found_channels.invoke({kFibreInvalidArgument, nullptr, nullptr, 0});
return; // TODO: error reporting
}
if (!try_parse_key(specs, specs + specs_len, "address", &address_begin, &address_end)) {
FIBRE_LOG(E) << "no address specified";
//on_found_channels.invoke({kFibreInvalidArgument, nullptr, nullptr, 0});
return; // TODO: error reporting
}
if (!try_parse_key(specs, specs + specs_len, "port", &port)) {
FIBRE_LOG(E) << "no port specified";
//on_found_channels.invoke({kFibreInvalidArgument, nullptr, nullptr, 0});
return; // TODO: error reporting
}
n_discoveries_++;
TcpChannelDiscoveryContext* ctx = new TcpChannelDiscoveryContext(); // TODO: free
ctx->parent = this;
ctx->address = {{address_begin, address_end}, port};
ctx->domain = domain;
ctx->resolve_address();
}
int PosixTcpBackend::stop_channel_discovery(ChannelDiscoveryContext* handle) {
// TODO
n_discoveries_--;
return 0;
}
void PosixTcpBackend::TcpChannelDiscoveryContext::resolve_address() {
if (addr_resolution_ctx) {
FIBRE_LOG(E) << "already resolving";
return;
}
if (!start_resolving_address(parent->event_loop_, address, false, &addr_resolution_ctx, MEMBER_CB(this, on_found_address))) {
FIBRE_LOG(E) << "cannot start address resolution";
return;
}
}
void PosixTcpBackend::TcpChannelDiscoveryContext::on_found_address(std::optional<cbufptr_t> addr) {
FIBRE_LOG(D) << "found address";
if (addr.has_value()) {
// Resolved an address. If it wasn't already known, try to connect to it.
std::vector<uint8_t> vec{addr->begin(), addr->end()};
bool is_known = std::find_if(known_addresses.begin(), known_addresses.end(),
[&](AddrContext& val){ return val.addr == vec; }) != known_addresses.end();
if (!is_known) {
AddrContext ctx = {.addr = vec};
if (parent->start_opening_connections(parent->event_loop_, *addr, SOCK_STREAM, IPPROTO_TCP, &ctx.connection_ctx, MEMBER_CB(this, on_connected))) {
known_addresses.push_back(ctx);
} else {
// TODO
}
}
} else {
// No more addresses.
addr_resolution_ctx = nullptr;
if (known_addresses.size() == 0) {
// No addresses could be found. Try again using exponential backoff.
parent->event_loop_->call_later(lookup_period, MEMBER_CB(this, resolve_address));
lookup_period = std::min(lookup_period * 3.0f, 3600.0f); // exponential backoff with at most 1h period
} else {
// Some addresses are known from this lookup or from a previous
// lookup. Resolve addresses again in 1h.
parent->event_loop_->call_later(3600.0, MEMBER_CB(this, resolve_address));
}
}
}
void PosixTcpBackend::TcpChannelDiscoveryContext::on_connected(std::optional<socket_id_t> socket_id) {
if (socket_id.has_value()) {
auto socket = new PosixSocket{}; // TODO: free
if (socket->init(parent->event_loop_, *socket_id)) {
domain->add_channels({kFibreOk, socket, socket, SIZE_MAX});
return;
}
delete socket;
}
FIBRE_LOG(D) << "not connected";
// Try to reconnect soon
lookup_period = 1.0f;
resolve_address();
}
void PosixTcpBackend::TcpChannelDiscoveryContext::on_disconnected() {
lookup_period = 1.0f; // reset exponential backoff
resolve_address();
}
@@ -0,0 +1,80 @@
#ifndef __FIBRE_POSIX_TCP_BACKEND_HPP
#define __FIBRE_POSIX_TCP_BACKEND_HPP
#include <fibre/event_loop.hpp>
#include "posix_socket.hpp"
#include <fibre/channel_discoverer.hpp>
#include <string>
#include <netdb.h>
namespace fibre {
/**
* TCP client and TCP server implementations are identical up to the function
* that is used to convert an address to one or more connected socket IDs.
* The client uses the posix function `connect` to do so, while the server uses
* the posix functions `listen` and `accept`.
*/
class PosixTcpBackend : public ChannelDiscoverer {
public:
bool init(EventLoop* event_loop);
bool deinit();
void start_channel_discovery(Domain* domain, const char* specs, size_t specs_len, ChannelDiscoveryContext** handle) final;
int stop_channel_discovery(ChannelDiscoveryContext* handle) final;
private:
struct TcpChannelDiscoveryContext {
PosixTcpBackend* parent;
std::tuple<std::string, int> address;
Domain* domain;
AddressResolutionContext* addr_resolution_ctx;
ConnectionContext* connection_ctx;
float lookup_period = 1.0f; // wait 1s for next address resolution
struct AddrContext {
std::vector<uint8_t> addr;
ConnectionContext* connection_ctx;
};
std::vector<AddrContext> known_addresses;
void resolve_address();
void on_found_address(std::optional<cbufptr_t> addr);
void on_connected(std::optional<socket_id_t> socket_id);
void on_disconnected();
};
virtual bool start_opening_connections(EventLoop* event_loop, cbufptr_t addr, int type, int protocol, ConnectionContext** ctx, Callback<void, std::optional<socket_id_t>> on_connected) = 0;
virtual void cancel_opening_connections(ConnectionContext* ctx) = 0;
EventLoop* event_loop_ = nullptr;
size_t n_discoveries_ = 0;
};
class PosixTcpClientBackend : public PosixTcpBackend {
public:
constexpr static const char* get_name() { return "tcp-client"; }
bool start_opening_connections(EventLoop* event_loop, cbufptr_t addr, int type, int protocol, ConnectionContext** ctx, Callback<void, std::optional<socket_id_t>> on_connected) final {
return start_connecting(event_loop, addr, type, protocol, ctx, on_connected);
}
void cancel_opening_connections(ConnectionContext* ctx) final {
stop_connecting(ctx);
}
};
class PosixTcpServerBackend : public PosixTcpBackend {
public:
constexpr static const char* get_name() { return "tcp-server"; }
bool start_opening_connections(EventLoop* event_loop, cbufptr_t addr, int type, int protocol, ConnectionContext** ctx, Callback<void, std::optional<socket_id_t>> on_connected) final {
return start_listening(event_loop, addr, type, protocol, ctx, on_connected);
}
void cancel_opening_connections(ConnectionContext* ctx) final {
stop_listening(ctx);
}
};
}
#endif // __FIBRE_POSIX_TCP_BACKEND_HPP