diff options
Diffstat (limited to 'libusb/io.c')
-rw-r--r-- | libusb/io.c | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/libusb/io.c b/libusb/io.c new file mode 100644 index 0000000..47e7bba --- /dev/null +++ b/libusb/io.c @@ -0,0 +1,702 @@ +/* + * I/O functions for libusb + * Copyright (C) 2007 Daniel Drake <dsd@gentoo.org> + * Copyright (c) 2001 Johannes Erdfelt <johannes@erdfelt.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include <config.h> + +#include <errno.h> +#include <signal.h> +#include <stdint.h> +#include <string.h> +#include <stdlib.h> +#include <sys/select.h> +#include <sys/time.h> +#include <time.h> +#include <unistd.h> + +/* signalfd() support is present in glibc-2.7 onwards, but glibc-2.7 contains + * a bug where the header is neither installed or compilable. This will be + * fixed for glibc-2.8. */ +#if __GLIBC_PREREQ(2, 8) +#include <sys/signalfd.h> +#else +#include "signalfd.h" +#endif + +#include "libusbi.h" + +static int sigfd; +static int signum; + +/* this is a list of in-flight fp_urb_handles, sorted by timeout expiration. + * URBs to timeout the soonest are placed at the beginning of the list, URBs + * that will time out later are placed after, and urbs with infinite timeout + * are always placed at the very end. */ +static struct list_head flying_urbs; + +static int setup_signalfd(int _signum) +{ + sigset_t sigset; + if (_signum == 0) + _signum = SIGRTMIN; + fp_dbg("signal %d", _signum); + + sigemptyset(&sigset); + sigaddset(&sigset, _signum); + sigfd = signalfd(-1, &sigset, 0); + if (sigfd < 0) { + fp_err("signalfd failed, code=%d errno=%d", sigfd, errno); + return sigfd; + } + fp_dbg("got signalfd %d", sigfd); + signum = _signum; + + sigemptyset(&sigset); + sigaddset(&sigset, _signum); + return sigprocmask(SIG_BLOCK, &sigset, NULL); +} + +int fpi_io_init(int _signum) +{ + list_init(&flying_urbs); + return setup_signalfd(signum); +} + +void fpi_io_exit(void) +{ + close(sigfd); +} + +static int calculate_timeout(struct libusb_urb_handle *urbh, + unsigned int timeout) +{ + int r; + struct timespec current_time; + struct sigevent sigevt = { + .sigev_notify = SIGEV_SIGNAL, + .sigev_signo = signum, + }; + struct itimerspec itspec; + struct timespec *it_value = &itspec.it_value; + + if (!timeout) + return 0; + + r = clock_gettime(CLOCK_MONOTONIC, ¤t_time); + if (r < 0) { + fp_err("failed to read monotonic clock, errno=%d", errno); + return r; + } + + r = timer_create(CLOCK_MONOTONIC, &sigevt, &urbh->timer); + if (r < 0) { + fp_err("failed to create monotonic timer"); + return r; + } + + memset(&itspec, 0, sizeof(itspec)); + it_value->tv_sec = current_time.tv_sec + (timeout / 1000); + it_value->tv_nsec = current_time.tv_nsec + + ((timeout % 1000) * 1000000); + + if (it_value->tv_nsec > 1000000000) { + it_value->tv_nsec -= 1000000000; + it_value->tv_sec++; + } + + r = timer_settime(&urbh->timer, TIMER_ABSTIME, &itspec, NULL); + if (r < 0) { + fp_err("failed to arm monotonic timer"); + return r; + } + + urbh->timeout = itspec.it_value; + + return 0; +} + +static void add_to_flying_list(struct libusb_urb_handle *urbh) +{ + struct libusb_urb_handle *cur; + struct timespec *timeout = &urbh->timeout; + + /* if we have no other flying urbs, start the list with this one */ + if (list_empty(&flying_urbs)) { + list_add(&urbh->list, &flying_urbs); + return; + } + + /* if we have infinite timeout, append to end of list */ + if (!TIMESPEC_IS_SET(timeout)) { + list_add_tail(&urbh->list, &flying_urbs); + return; + } + + /* otherwise, find appropriate place in list */ + list_for_each_entry(cur, &flying_urbs, list) { + /* find first timeout that occurs after the urbh in question */ + struct timespec *cur_ts = &cur->timeout; + + if (!TIMESPEC_IS_SET(cur_ts) || (cur_ts->tv_sec > timeout->tv_sec) || + (cur_ts->tv_sec == timeout->tv_sec && + cur_ts->tv_nsec > timeout->tv_nsec)) { + list_add_tail(&urbh->list, &cur->list); + return; + } + } + + /* otherwise we need to be inserted at the end */ + list_add_tail(&urbh->list, &flying_urbs); +} + +static int submit_urb(struct libusb_dev_handle *devh, + struct libusb_urb_handle *urbh) +{ + int r; + struct usb_urb *urb = &urbh->urb; + int to_be_transferred = urbh->transfer_len - urbh->transferred; + + urb->type = urbh->urb_type; + urb->endpoint = urbh->endpoint; + urb->buffer = urbh->buffer + urbh->transferred; + urb->buffer_length = MIN(to_be_transferred, MAX_URB_BUFFER_LENGTH); + urb->signr = signum; + + /* FIXME: for requests that we have to split into multiple URBs, we should + * submit all the URBs instantly: submit, submit, submit, reap, reap, reap + * rather than: submit, reap, submit, reap, submit, reap + * this will improve performance and fix bugs concerning behaviour when + * the user submits two similar multiple-urb requests */ + fp_dbg("transferring %d from %d bytes", urb->buffer_length, + to_be_transferred); + + r = ioctl(devh->fd, IOCTL_USB_SUBMITURB, &urbh->urb); + if (r < 0) { + fp_err("submiturb failed error %d errno=%d", r, errno); + return r; + } + + add_to_flying_list(urbh); + return 0; +} + +API_EXPORTED struct libusb_urb_handle *libusb_submit_ctrl_msg( + struct libusb_dev_handle *devh, struct libusb_ctrl_msg *msg, + libusb_ctrl_cb_fn callback, void *user_data, unsigned int timeout) +{ + struct libusb_urb_handle *urbh = malloc(sizeof(*urbh)); + struct usb_ctrl_setup *setup; + unsigned char *urbdata; + int urbdata_length = sizeof(struct usb_ctrl_setup) + msg->length; + int r; + + if (!urbh) + return NULL; + memset(urbh, 0, sizeof(*urbh)); + urbh->devh = devh; + urbh->callback = callback; + urbh->user_data = user_data; + r = calculate_timeout(urbh, timeout); + if (r < 0) { + free(urbh); + return NULL; + } + + urbdata = malloc(urbdata_length); + if (!urbdata) { + free(urbh); + return NULL; + } + + fp_dbg("RQT=%02x RQ=%02x VAL=%04x IDX=%04x length=%d", + msg->requesttype, msg->request, msg->value, msg->index, msg->length); + + setup = (struct usb_ctrl_setup *) urbdata; + setup->bRequestType = msg->requesttype; + setup->bRequest = msg->request; + setup->wValue = cpu_to_le16(msg->value); + setup->wIndex = cpu_to_le16(msg->index); + setup->wLength = cpu_to_le16(msg->length); + + if ((msg->requesttype & 0x80) == USB_ENDPOINT_OUT) + memcpy(urbdata + sizeof(struct usb_ctrl_setup), msg->data, msg->length); + + urbh->urb_type = USB_URB_TYPE_CONTROL; + urbh->buffer = urbdata; + urbh->transfer_len = urbdata_length; + + r = submit_urb(devh, urbh); + if (r < 0) { + free(urbh); + free(urbdata); + return NULL; + } + + return urbh; +} + +static struct libusb_urb_handle *submit_bulk_msg(struct libusb_dev_handle *devh, + struct libusb_bulk_msg *msg, libusb_bulk_cb_fn callback, void *user_data, + unsigned int timeout, unsigned char urbtype) +{ + struct libusb_urb_handle *urbh = malloc(sizeof(*urbh)); + int r; + + fp_dbg("length %d timeout %d", msg->length, timeout); + + if (!urbh) + return NULL; + memset(urbh, 0, sizeof(*urbh)); + r = calculate_timeout(urbh, timeout); + if (r < 0) { + free(urbh); + return NULL; + } + urbh->devh = devh; + urbh->callback = callback; + urbh->user_data = user_data; + urbh->flags |= LIBUSB_URBH_DATA_BELONGS_TO_USER; + urbh->endpoint = msg->endpoint; + urbh->urb_type = urbtype; + urbh->buffer = msg->data; + urbh->transfer_len = msg->length; + + r = submit_urb(devh, urbh); + if (r < 0) { + free(urbh); + return NULL; + } + + return urbh; +} + +API_EXPORTED struct libusb_urb_handle *libusb_submit_bulk_msg( + struct libusb_dev_handle *devh, struct libusb_bulk_msg *msg, + libusb_bulk_cb_fn callback, void *user_data, unsigned int timeout) +{ + return submit_bulk_msg(devh, msg, callback, user_data, timeout, + USB_URB_TYPE_BULK); +} + +API_EXPORTED struct libusb_urb_handle *libusb_submit_intr_msg( + struct libusb_dev_handle *devh, struct libusb_bulk_msg *msg, + libusb_bulk_cb_fn callback, void *user_data, unsigned int timeout) +{ + return submit_bulk_msg(devh, msg, callback, user_data, timeout, + USB_URB_TYPE_INTERRUPT); +} + +API_EXPORTED int libusb_urb_handle_cancel(struct libusb_dev_handle *devh, + struct libusb_urb_handle *urbh) +{ + int r; + fp_dbg(""); + r = ioctl(devh->fd, IOCTL_USB_DISCARDURB, &urbh->urb); + if (r < 0) + fp_err("cancel urb failed error %d", r); + return r; +} + +API_EXPORTED int libusb_urb_handle_cancel_sync(struct libusb_dev_handle *devh, + struct libusb_urb_handle *urbh) +{ + int r; + fp_dbg(""); + r = ioctl(devh->fd, IOCTL_USB_DISCARDURB, &urbh->urb); + if (r < 0) { + fp_err("cancel urb failed error %d", r); + return r; + } + + urbh->flags |= LIBUSB_URBH_SYNC_CANCELLED; + while (urbh->flags & LIBUSB_URBH_SYNC_CANCELLED) { + r = libusb_poll(); + if (r < 0) + return r; + } + + return 0; +} + +int handle_transfer_completion(struct libusb_dev_handle *devh, + struct libusb_urb_handle *urbh, enum fp_urb_cb_status status) +{ + struct usb_urb *urb = &urbh->urb; + + if (TIMESPEC_IS_SET(&urbh->timeout)) + timer_delete(urbh->timer); + + if (status == FP_URB_SILENT_COMPLETION) + return 0; + + if (urb->type == USB_URB_TYPE_CONTROL) { + libusb_ctrl_cb_fn callback = urbh->callback; + if (callback) + callback(devh, urbh, status, urb->buffer, + urb->buffer + sizeof(struct usb_ctrl_setup), urbh->transferred, + urbh->user_data); + } else if (urb->type == USB_URB_TYPE_BULK || + urb->type == USB_URB_TYPE_INTERRUPT) { + libusb_bulk_cb_fn callback = urbh->callback; + if (callback) + callback(devh, urbh, status, urbh->endpoint, urbh->transfer_len, + urbh->buffer, urbh->transferred, urbh->user_data); + } + return 0; +} + +static int handle_transfer_cancellation(struct libusb_dev_handle *devh, + struct libusb_urb_handle *urbh) +{ + /* if the URB is being cancelled synchronously, raise cancellation + * completion event by unsetting flag, and ensure that user callback does + * not get called. + */ + if (urbh->flags & LIBUSB_URBH_SYNC_CANCELLED) { + urbh->flags &= ~LIBUSB_URBH_SYNC_CANCELLED; + fp_dbg("detected sync. cancel"); + return handle_transfer_completion(devh, urbh, FP_URB_SILENT_COMPLETION); + } + + /* if the URB was cancelled due to timeout, report timeout to the user */ + if (urbh->flags & LIBUSB_URBH_TIMED_OUT) { + fp_dbg("detected timeout cancellation"); + return handle_transfer_completion(devh, urbh, FP_URB_TIMEOUT); + } + + /* otherwise its a normal async cancel */ + return handle_transfer_completion(devh, urbh, FP_URB_CANCELLED); +} + +static int reap_for_devh(struct libusb_dev_handle *devh) +{ + int r; + struct usb_urb *urb; + struct libusb_urb_handle *urbh; + int trf_requested; + + r = ioctl(devh->fd, IOCTL_USB_REAPURBNDELAY, &urb); + if (r == -1 && errno == EAGAIN) + return r; + if (r < 0) { + fp_err("reap failed error %d errno=%d", r, errno); + return r; + } + + urbh = container_of(urb, struct libusb_urb_handle, urb); + + fp_dbg("urb type=%d status=%d transferred=%d", urb->type, urb->status, + urb->actual_length); + list_del(&urbh->list); + + if (urb->status == -2) + return handle_transfer_cancellation(devh, urbh); + /* FIXME: research what other status codes may exist */ + if (urb->status != 0) + fp_warn("unrecognised urb status %d", urb->status); + + /* determine how much data was asked for */ + trf_requested = MIN(urbh->transfer_len - urbh->transferred, + MAX_URB_BUFFER_LENGTH); + + urbh->transferred += urb->actual_length; + + /* if we were provided less data than requested, then our transfer is + * done */ + if (urb->actual_length < trf_requested) { + fp_dbg("less data than requested (%d/%d) --> all done", + urb->actual_length, trf_requested); + return handle_transfer_completion(devh, urbh, FP_URB_COMPLETED); + } + + /* if we've transferred all data, we're done */ + if (urbh->transferred == urbh->transfer_len) { + fp_dbg("transfer complete --> all done"); + return handle_transfer_completion(devh, urbh, FP_URB_COMPLETED); + } + + /* otherwise, we have more data to transfer */ + fp_dbg("more data to transfer..."); + memset(urb, 0, sizeof(*urb)); + return submit_urb(devh, urbh); +} + +static void handle_timeout(struct libusb_urb_handle *urbh) +{ + /* handling timeouts is tricky, as we may race with the kernel: we may + * detect a timeout racing with the condition that the urb has actually + * completed. we asynchronously cancel the URB and report timeout + * to the user when the URB cancellation completes (or not at all if the + * URB actually gets delivered as per this race) */ + int r; + + + urbh->flags |= LIBUSB_URBH_TIMED_OUT; + r = libusb_urb_handle_cancel(urbh->devh, urbh); + if (r < 0) + fp_warn("async cancel failed %d errno=%d", r, errno); +} + +static int handle_timeouts(void) +{ + struct timespec systime; + struct libusb_urb_handle *urbh; + int r; + + if (list_empty(&flying_urbs)) + return 0; + + /* get current time */ + r = clock_gettime(CLOCK_MONOTONIC, &systime); + if (r < 0) + return r; + + /* iterate through flying urbs list, finding all urbs that have expired + * timeouts */ + list_for_each_entry(urbh, &flying_urbs, list) { + struct timespec *cur_ts = &urbh->timeout; + + /* if we've reached urbs of infinite timeout, we're all done */ + if (!TIMESPEC_IS_SET(cur_ts)) + return 0; + + /* if urb has non-expired timeout, nothing more to do */ + if ((cur_ts->tv_sec > systime.tv_sec) || + (cur_ts->tv_sec == systime.tv_sec && + cur_ts->tv_nsec > systime.tv_nsec)) + return 0; + + /* otherwise, we've got an expired timeout to handle */ + handle_timeout(urbh); + } + + return 0; +} + +static int reap(void) +{ + struct libusb_dev_handle *devh; + int r; + + list_for_each_entry(devh, &open_devs, list) { + r = reap_for_devh(devh); + if (r == -1 && errno == EAGAIN) + continue; + if (r < 0) + return r; + } + + r = handle_timeouts(); + + return 0; +} + +static int flush_sigfd(void) +{ + int r; + struct signalfd_siginfo siginfo; + r = read(sigfd, &siginfo, sizeof(siginfo)); + if (r < 0) { + fp_err("sigfd read failed %d %d", r, errno); + return r; + } + if ((unsigned int) r < sizeof(siginfo)) { + fp_err("sigfd short read (%d/%d)", r, sizeof(siginfo)); + return -1; + } + return 0; +} + +static int poll_io(struct timeval *tv) +{ + int r; + fd_set fds; + + FD_ZERO(&fds); + FD_SET(sigfd, &fds); + r = select(sigfd + 1, &fds, NULL, NULL, tv); + if (r == -1 && errno == EINTR) + return 0; + if (r < 0) { + fp_err("select failed %d err=%d\n", r, errno); + return r; + } + + if (r > 0) { + flush_sigfd(); + return reap(); + } + + return 0; +} + +API_EXPORTED int libusb_poll_timeout(struct timeval *tv) +{ + return poll_io(tv); +} + +API_EXPORTED int libusb_poll(void) +{ + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 500000; + return poll_io(&tv); +} + +struct sync_ctrl_handle { + enum fp_urb_cb_status status; + unsigned char *data; + int actual_length; +}; + +static void ctrl_msg_cb(struct libusb_dev_handle *devh, + struct libusb_urb_handle *urbh, enum fp_urb_cb_status status, + struct usb_ctrl_setup *setup, unsigned char *data, int actual_length, + void *user_data) +{ + struct sync_ctrl_handle *ctrlh = (struct sync_ctrl_handle *) user_data; + fp_dbg("actual_length=%d", actual_length); + + if (status == FP_URB_COMPLETED) { + /* copy results into user-defined buffer */ + if (setup->bRequestType & USB_ENDPOINT_IN) + memcpy(ctrlh->data, data, actual_length); + } + + ctrlh->status = status; + ctrlh->actual_length = actual_length; + /* caller frees urbh */ +} + +API_EXPORTED int libusb_ctrl_msg(struct libusb_dev_handle *devh, + struct libusb_ctrl_msg *msg, unsigned int timeout) +{ + struct libusb_urb_handle *urbh; + struct sync_ctrl_handle ctrlh; + + memset(&ctrlh, 0, sizeof(ctrlh)); + ctrlh.data = msg->data; + + urbh = libusb_submit_ctrl_msg(devh, msg, ctrl_msg_cb, &ctrlh, timeout); + if (!urbh) + return -1; + + while (!ctrlh.status) { + int r = libusb_poll(); + if (r < 0) { + libusb_urb_handle_cancel_sync(devh, urbh); + libusb_urb_handle_free(urbh); + return r; + } + } + + libusb_urb_handle_free(urbh); + switch (ctrlh.status) { + case FP_URB_COMPLETED: + return ctrlh.actual_length; + case FP_URB_TIMEOUT: + return -ETIMEDOUT; + default: + fp_warn("unrecognised status code %d", ctrlh.status); + return -1; + } +} + +struct sync_bulk_handle { + enum fp_urb_cb_status status; + int actual_length; +}; + +static void bulk_msg_cb(struct libusb_dev_handle *devh, + struct libusb_urb_handle *urbh, enum fp_urb_cb_status status, + unsigned char endpoint, int rqlength, unsigned char *data, + int actual_length, void *user_data) +{ + struct sync_bulk_handle *bulkh = (struct sync_bulk_handle *) user_data; + fp_dbg(""); + bulkh->status = status; + bulkh->actual_length = actual_length; + /* caller frees urbh */ +} + +static int do_sync_bulk_msg(struct libusb_dev_handle *devh, + struct libusb_bulk_msg *msg, int *transferred, unsigned int timeout, + unsigned char urbtype) +{ + struct libusb_urb_handle *urbh; + struct sync_bulk_handle bulkh; + + memset(&bulkh, 0, sizeof(bulkh)); + + urbh = submit_bulk_msg(devh, msg, bulk_msg_cb, &bulkh, timeout, urbtype); + if (!urbh) + return -1; + + while (!bulkh.status) { + int r = libusb_poll(); + if (r < 0) { + libusb_urb_handle_cancel_sync(devh, urbh); + libusb_urb_handle_free(urbh); + return r; + } + } + + *transferred = bulkh.actual_length; + libusb_urb_handle_free(urbh); + + switch (bulkh.status) { + case FP_URB_COMPLETED: + return 0; + case FP_URB_TIMEOUT: + return -ETIMEDOUT; + default: + fp_warn("unrecognised status code %d", bulkh.status); + return -1; + } +} + +API_EXPORTED int libusb_intr_msg(struct libusb_dev_handle *devh, + struct libusb_bulk_msg *msg, int *transferred, unsigned int timeout) +{ + return do_sync_bulk_msg(devh, msg, transferred, timeout, + USB_URB_TYPE_INTERRUPT); +} + +API_EXPORTED int libusb_bulk_msg(struct libusb_dev_handle *devh, + struct libusb_bulk_msg *msg, int *transferred, unsigned int timeout) +{ + return do_sync_bulk_msg(devh, msg, transferred, timeout, + USB_URB_TYPE_BULK); +} + +API_EXPORTED void libusb_urb_handle_free(struct libusb_urb_handle *urbh) +{ + if (!urbh) + return; + + if (!(urbh->flags & LIBUSB_URBH_DATA_BELONGS_TO_USER)) + free(urbh->urb.buffer); + free(urbh); +} + +API_EXPORTED int libusb_get_pollfd(void) +{ + return sigfd; +} + |