*
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user