/* * Copyright (c) 2018 Ondrej Hlavaty * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * - The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** @addtogroup libusb * @{ */ /** @file * An USB hub port state machine. * * This helper structure solves a repeated problem in USB world: management of * USB ports. A port is an object which receives events (connect, disconnect, * reset) which are to be handled in an asynchronous way. The tricky part is * that response to events has to wait for different events - the most notable * being USB 2 port requiring port reset to be enabled. This problem is solved * by launching separate fibril for taking the port up. * * This subsystem abstracts the rather complicated state machine, and offers * a simple interface to announce events and leave the fibril management on the * library. */ #include #include #include #include #include void usb_port_init(usb_port_t *port) { fibril_mutex_initialize(&port->guard); fibril_condvar_initialize(&port->finished_cv); fibril_condvar_initialize(&port->enabled_cv); } struct enumerate_worker_args { usb_port_t *port; usb_port_enumerate_t handler; }; static int enumerate_worker(void *arg) { struct enumerate_worker_args *const args = arg; usb_port_t *port = args->port; usb_port_enumerate_t handler = args->handler; free(args); fibril_mutex_lock(&port->guard); if (port->state == PORT_ERROR) { /* * The device was removed faster than this fibril acquired the * mutex. */ port->state = PORT_DISABLED; goto out; } assert(port->state == PORT_CONNECTING); port->state = handler(port) ? PORT_DISABLED : PORT_ENUMERATED; out: fibril_condvar_broadcast(&port->finished_cv); fibril_mutex_unlock(&port->guard); return EOK; // This is a fibril worker. No one will read the value. } int usb_port_connected(usb_port_t *port, usb_port_enumerate_t handler) { assert(port); int ret = ENOMEM; fibril_mutex_lock(&port->guard); if (port->state != PORT_DISABLED) { usb_log_warning("a connected event come for port that is not disabled."); ret = EINVAL; goto out; } struct enumerate_worker_args *args = malloc(sizeof(*args)); if (!args) goto out; fid_t fibril = fibril_create(&enumerate_worker, args); if (!fibril) { free(args); goto out; } args->port = port; args->handler = handler; port->state = PORT_CONNECTING; fibril_add_ready(fibril); out: fibril_mutex_unlock(&port->guard); return ret; } void usb_port_enabled(usb_port_t *port) { assert(port); fibril_mutex_lock(&port->guard); fibril_condvar_broadcast(&port->enabled_cv); fibril_mutex_unlock(&port->guard); } struct remove_worker_args { usb_port_t *port; usb_port_remove_t handler; }; static int remove_worker(void *arg) { struct remove_worker_args *const args = arg; usb_port_t *port = args->port; usb_port_remove_t handler = args->handler; free(args); fibril_mutex_lock(&port->guard); assert(port->state == PORT_DISCONNECTING); handler(port); port->state = PORT_DISABLED; fibril_condvar_broadcast(&port->finished_cv); fibril_mutex_unlock(&port->guard); return EOK; } static void fork_remove_worker(usb_port_t *port, usb_port_remove_t handler) { struct remove_worker_args *args = malloc(sizeof(*args)); if (!args) return; fid_t fibril = fibril_create(&remove_worker, args); if (!fibril) { free(args); return; } args->port = port; args->handler = handler; port->state = PORT_DISCONNECTING; fibril_add_ready(fibril); } void usb_port_disabled(usb_port_t *port, usb_port_remove_t handler) { assert(port); fibril_mutex_lock(&port->guard); switch (port->state) { case PORT_ENUMERATED: fork_remove_worker(port, handler); break; case PORT_CONNECTING: port->state = PORT_ERROR; fibril_condvar_broadcast(&port->enabled_cv); /* fallthrough */ case PORT_ERROR: fibril_condvar_wait(&port->finished_cv, &port->guard); /* fallthrough */ case PORT_DISCONNECTING: case PORT_DISABLED: break; } fibril_mutex_unlock(&port->guard); } void usb_port_fini(usb_port_t *port) { assert(port); fibril_mutex_lock(&port->guard); switch (port->state) { case PORT_ENUMERATED: /* * We shall inform the HC that the device is gone. * However, we can't wait for it, because if the device is hub, * it would have to use the same IPC handling fibril as we do. * But we cannot even defer it to another fibril, because then * the HC would assume our driver didn't cleanup properly, and * will remove those devices by himself. * * So the solutions seems to be to behave like a bad driver and * leave the work for HC. */ port->state = PORT_DISABLED; /* fallthrough */ case PORT_DISABLED: break; case PORT_CONNECTING: /* We first have to stop the fibril in progress. */ port->state = PORT_ERROR; fibril_condvar_broadcast(&port->enabled_cv); /* fallthrough */ case PORT_ERROR: case PORT_DISCONNECTING: fibril_condvar_wait(&port->finished_cv, &port->guard); break; } fibril_mutex_unlock(&port->guard); } int usb_port_condvar_wait_timeout(usb_port_t *port, fibril_condvar_t *cv, usec_t timeout) { assert(port); assert(port->state == PORT_CONNECTING); assert(fibril_mutex_is_locked(&port->guard)); if (fibril_condvar_wait_timeout(cv, &port->guard, timeout)) return ETIMEOUT; return port->state == PORT_CONNECTING ? EOK : EINTR; } /** * @} */