darwin: fix behavior of libusb_set_interface_alt_setting when it stalls the pipe

In some versions of macOS a pipe stall returned when setting the alternate interface
causes the interface to become unusable. To handle this case the backend was always
re-claiming the interface before clearing the pipe stall. In macOS Monterey
unconditionally re-claiming the interface leads to an error due to the process already
having exclusive access. To resolve this issue we attempt to clear the halt and only
re-claim the interface if clearing the pipe stall returns kIOUSBUnknownPipeErr. Tested
with 12.0.1 and 10.13 and get the expected results in both cases with a custom USB
device that has this behavior.

Fixes #1011
Closes #1031

Signed-off-by: Nathan Hjelm <hjelmn@google.com>
diff --git a/libusb/os/darwin_usb.c b/libusb/os/darwin_usb.c
index 15457f7..5f2fa3c 100644
--- a/libusb/os/darwin_usb.c
+++ b/libusb/os/darwin_usb.c
@@ -1,8 +1,8 @@
 /* -*- Mode: C; indent-tabs-mode:nil -*- */
 /*
  * darwin backend for libusb 1.0
- * Copyright © 2008-2020 Nathan Hjelm <hjelmn@cs.unm.edu>
- * Copyright © 2019-2020 Google LLC. All rights reserved.
+ * Copyright © 2008-2021 Nathan Hjelm <hjelmn@cs.unm.edu>
+ * Copyright © 2019-2021 Google LLC. All rights reserved.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -160,11 +160,12 @@
     return LIBUSB_ERROR_INVALID_PARAM;
   case kIOUSBTransactionTimeout:
     return LIBUSB_ERROR_TIMEOUT;
+  case kIOUSBUnknownPipeErr:
+    return LIBUSB_ERROR_NOT_FOUND;
   case kIOReturnNotResponding:
   case kIOReturnAborted:
   case kIOReturnError:
   case kIOUSBNoAsyncPortErr:
-  case kIOUSBUnknownPipeErr:
   default:
     return LIBUSB_ERROR_OTHER;
   }
@@ -1586,12 +1587,34 @@
   return darwin_to_libusb (kresult);
 }
 
+static int check_alt_setting_and_clear_halt(struct libusb_device_handle *dev_handle, uint8_t altsetting, struct darwin_interface *cInterface) {
+  enum libusb_error ret;
+  IOReturn kresult;
+  uint8_t current_alt_setting;
+
+  kresult = (*(cInterface->interface))->GetAlternateSetting (cInterface->interface, &current_alt_setting);
+  if (kresult == kIOReturnSuccess && altsetting != current_alt_setting) {
+    return LIBUSB_ERROR_PIPE;
+  }
+
+  for (int i = 0 ; i < cInterface->num_endpoints ; i++) {
+    ret = darwin_clear_halt(dev_handle, cInterface->endpoint_addrs[i]);
+    if (LIBUSB_SUCCESS != ret) {
+      usbi_warn(HANDLE_CTX (dev_handle), "error clearing pipe halt for endpoint %d", i);
+      if (LIBUSB_ERROR_NOT_FOUND == ret) {
+        /* may need to re-open the interface */
+        return ret;
+      }
+    }
+  }
+
+  return LIBUSB_SUCCESS;
+}
+
 static int darwin_set_interface_altsetting(struct libusb_device_handle *dev_handle, uint8_t iface, uint8_t altsetting) {
   struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);
   IOReturn kresult;
   enum libusb_error ret;
-  int i;
-  uint8_t old_alt_setting;
 
   /* current interface */
   struct darwin_interface *cInterface = &priv->interfaces[iface];
@@ -1623,23 +1646,18 @@
      Mimic the behaviour in e.g. the Linux kernel: in such case, reset all endpoints
      of the interface (as would have been done per 9.1.1.5) and return success. */
 
-  /* For some reason we need to reclaim the interface after the pipe error */
-  ret = darwin_claim_interface (dev_handle, iface);
-
-  if (ret) {
-    darwin_release_interface (dev_handle, iface);
-    usbi_err (HANDLE_CTX (dev_handle), "could not reclaim interface");
+  ret = check_alt_setting_and_clear_halt(dev_handle, altsetting, cInterface);
+  if (LIBUSB_ERROR_NOT_FOUND == ret) {
+    /* For some reason we need to reclaim the interface after the pipe error with some versions of macOS */
+    ret = darwin_claim_interface (dev_handle, iface);
+    if (LIBUSB_SUCCESS != ret) {
+      darwin_release_interface (dev_handle, iface);
+      usbi_err (HANDLE_CTX (dev_handle), "could not reclaim interface: %s", darwin_error_str(kresult));
+    }
+    ret = check_alt_setting_and_clear_halt(dev_handle, altsetting, cInterface);
   }
 
-  /* Return error if a change to another value was attempted */
-  kresult = (*(cInterface->interface))->GetAlternateSetting (cInterface->interface, &old_alt_setting);
-  if (kresult == kIOReturnSuccess && altsetting != old_alt_setting)
-    return LIBUSB_ERROR_PIPE;
-
-  for (i = 0 ; i < cInterface->num_endpoints ; i++)
-    darwin_clear_halt(dev_handle, cInterface->endpoint_addrs[i]);
-
-  return LIBUSB_SUCCESS;
+  return ret;
 }
 
 static int darwin_clear_halt(struct libusb_device_handle *dev_handle, unsigned char endpoint) {
diff --git a/libusb/version_nano.h b/libusb/version_nano.h
index 04fa88c..09bf359 100644
--- a/libusb/version_nano.h
+++ b/libusb/version_nano.h
@@ -1 +1 @@
-#define LIBUSB_NANO 11679
+#define LIBUSB_NANO 11680