[PATCH] usbhid: add error handling
This patch (as628c) adds error handling to the USB HID core. When an
error is reported for an interrupt URB, the driver will do delayed
retries, at increasing intervals, for up to one second. If that doesn't
work, it will try to reset the device. Testing by users has shown that
both the retries and the resets end up getting used.
Signed-off-by: Alan Stern <stern@rowland.harvard.edu>
Signed-off-by: Greg Kroah-Hartman <gregkh@suse.de>
diff --git a/drivers/usb/input/hid-core.c b/drivers/usb/input/hid-core.c
index f187a71..6b8a51c 100644
--- a/drivers/usb/input/hid-core.c
+++ b/drivers/usb/input/hid-core.c
@@ -903,6 +903,99 @@
}
/*
+ * Input submission and I/O error handler.
+ */
+
+static void hid_io_error(struct hid_device *hid);
+
+/* Start up the input URB */
+static int hid_start_in(struct hid_device *hid)
+{
+ unsigned long flags;
+ int rc = 0;
+
+ spin_lock_irqsave(&hid->inlock, flags);
+ if (hid->open > 0 && !test_bit(HID_SUSPENDED, &hid->iofl) &&
+ !test_and_set_bit(HID_IN_RUNNING, &hid->iofl)) {
+ rc = usb_submit_urb(hid->urbin, GFP_ATOMIC);
+ if (rc != 0)
+ clear_bit(HID_IN_RUNNING, &hid->iofl);
+ }
+ spin_unlock_irqrestore(&hid->inlock, flags);
+ return rc;
+}
+
+/* I/O retry timer routine */
+static void hid_retry_timeout(unsigned long _hid)
+{
+ struct hid_device *hid = (struct hid_device *) _hid;
+
+ dev_dbg(&hid->intf->dev, "retrying intr urb\n");
+ if (hid_start_in(hid))
+ hid_io_error(hid);
+}
+
+/* Workqueue routine to reset the device */
+static void hid_reset(void *_hid)
+{
+ struct hid_device *hid = (struct hid_device *) _hid;
+ int rc_lock, rc;
+
+ dev_dbg(&hid->intf->dev, "resetting device\n");
+ rc = rc_lock = usb_lock_device_for_reset(hid->dev, hid->intf);
+ if (rc_lock >= 0) {
+ rc = usb_reset_device(hid->dev);
+ if (rc_lock)
+ usb_unlock_device(hid->dev);
+ }
+ clear_bit(HID_RESET_PENDING, &hid->iofl);
+
+ if (rc == 0) {
+ hid->retry_delay = 0;
+ if (hid_start_in(hid))
+ hid_io_error(hid);
+ } else if (!(rc == -ENODEV || rc == -EHOSTUNREACH || rc == -EINTR))
+ err("can't reset device, %s-%s/input%d, status %d",
+ hid->dev->bus->bus_name,
+ hid->dev->devpath,
+ hid->ifnum, rc);
+}
+
+/* Main I/O error handler */
+static void hid_io_error(struct hid_device *hid)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&hid->inlock, flags);
+
+ /* Stop when disconnected */
+ if (usb_get_intfdata(hid->intf) == NULL)
+ goto done;
+
+ /* When an error occurs, retry at increasing intervals */
+ if (hid->retry_delay == 0) {
+ hid->retry_delay = 13; /* Then 26, 52, 104, 104, ... */
+ hid->stop_retry = jiffies + msecs_to_jiffies(1000);
+ } else if (hid->retry_delay < 100)
+ hid->retry_delay *= 2;
+
+ if (time_after(jiffies, hid->stop_retry)) {
+
+ /* Retries failed, so do a port reset */
+ if (!test_and_set_bit(HID_RESET_PENDING, &hid->iofl)) {
+ if (schedule_work(&hid->reset_work))
+ goto done;
+ clear_bit(HID_RESET_PENDING, &hid->iofl);
+ }
+ }
+
+ mod_timer(&hid->io_retry,
+ jiffies + msecs_to_jiffies(hid->retry_delay));
+done:
+ spin_unlock_irqrestore(&hid->inlock, flags);
+}
+
+/*
* Input interrupt completion handler.
*/
@@ -913,25 +1006,35 @@
switch (urb->status) {
case 0: /* success */
+ hid->retry_delay = 0;
hid_input_report(HID_INPUT_REPORT, urb, 1, regs);
break;
case -ECONNRESET: /* unlink */
case -ENOENT:
- case -EPERM:
case -ESHUTDOWN: /* unplug */
- case -EILSEQ: /* unplug timeout on uhci */
+ clear_bit(HID_IN_RUNNING, &hid->iofl);
return;
+ case -EILSEQ: /* protocol error or unplug */
+ case -EPROTO: /* protocol error or unplug */
case -ETIMEDOUT: /* NAK */
- break;
+ clear_bit(HID_IN_RUNNING, &hid->iofl);
+ hid_io_error(hid);
+ return;
default: /* error */
warn("input irq status %d received", urb->status);
}
status = usb_submit_urb(urb, SLAB_ATOMIC);
- if (status)
- err("can't resubmit intr, %s-%s/input%d, status %d",
- hid->dev->bus->bus_name, hid->dev->devpath,
- hid->ifnum, status);
+ if (status) {
+ clear_bit(HID_IN_RUNNING, &hid->iofl);
+ if (status != -EPERM) {
+ err("can't resubmit intr, %s-%s/input%d, status %d",
+ hid->dev->bus->bus_name,
+ hid->dev->devpath,
+ hid->ifnum, status);
+ hid_io_error(hid);
+ }
+ }
}
/*
@@ -1093,8 +1196,9 @@
case 0: /* success */
break;
case -ESHUTDOWN: /* unplug */
- case -EILSEQ: /* unplug timeout on uhci */
unplug = 1;
+ case -EILSEQ: /* protocol error or unplug */
+ case -EPROTO: /* protocol error or unplug */
case -ECONNRESET: /* unlink */
case -ENOENT:
break;
@@ -1141,8 +1245,9 @@
hid_input_report(hid->ctrl[hid->ctrltail].report->type, urb, 0, regs);
break;
case -ESHUTDOWN: /* unplug */
- case -EILSEQ: /* unplug timectrl on uhci */
unplug = 1;
+ case -EILSEQ: /* protocol error or unplug */
+ case -EPROTO: /* protocol error or unplug */
case -ECONNRESET: /* unlink */
case -ENOENT:
case -EPIPE: /* report not available */
@@ -1255,14 +1360,9 @@
int hid_open(struct hid_device *hid)
{
- if (hid->open++)
- return 0;
-
- hid->urbin->dev = hid->dev;
-
- if (usb_submit_urb(hid->urbin, GFP_KERNEL))
- return -EIO;
-
+ ++hid->open;
+ if (hid_start_in(hid))
+ hid_io_error(hid);
return 0;
}
@@ -1787,6 +1887,10 @@
init_waitqueue_head(&hid->wait);
+ INIT_WORK(&hid->reset_work, hid_reset, hid);
+ setup_timer(&hid->io_retry, hid_retry_timeout, (unsigned long) hid);
+
+ spin_lock_init(&hid->inlock);
spin_lock_init(&hid->outlock);
spin_lock_init(&hid->ctrllock);
@@ -1855,11 +1959,16 @@
if (!hid)
return;
+ spin_lock_irq(&hid->inlock); /* Sync with error handler */
usb_set_intfdata(intf, NULL);
+ spin_unlock_irq(&hid->inlock);
usb_kill_urb(hid->urbin);
usb_kill_urb(hid->urbout);
usb_kill_urb(hid->urbctrl);
+ del_timer_sync(&hid->io_retry);
+ flush_scheduled_work();
+
if (hid->claimed & HID_CLAIMED_INPUT)
hidinput_disconnect(hid);
if (hid->claimed & HID_CLAIMED_HIDDEV)
@@ -1934,6 +2043,10 @@
{
struct hid_device *hid = usb_get_intfdata (intf);
+ spin_lock_irq(&hid->inlock); /* Sync with error handler */
+ set_bit(HID_SUSPENDED, &hid->iofl);
+ spin_unlock_irq(&hid->inlock);
+ del_timer(&hid->io_retry);
usb_kill_urb(hid->urbin);
dev_dbg(&intf->dev, "suspend\n");
return 0;
@@ -1944,10 +2057,8 @@
struct hid_device *hid = usb_get_intfdata (intf);
int status;
- if (hid->open)
- status = usb_submit_urb(hid->urbin, GFP_NOIO);
- else
- status = 0;
+ clear_bit(HID_SUSPENDED, &hid->iofl);
+ status = hid_start_in(hid);
dev_dbg(&intf->dev, "resume status %d\n", status);
return status;
}