fastboot: Add ability to specify device path

For manufacturing and testing, there is a need to talk to
whatever device is connected to a given port on the host.  This
change modifies fastboot's "-s" option to take either a serial
number or a device path.  The device paths of the connected
devices can be listed using "fastboot -l devices" whose output
will resemble:

    016B75D60A00600D	usb:2-5	fastboot
    AD3C12020173	usb:1-4.3	fastboot

The second column lists the device paths.  If the -l option is
not given, the output from "fastboot devices" will be the same as
it used to be (i.e. the paths will not be printed).

Finally, note that the format of the device paths are platform
dependent.  The example above is from Linux.  On OS-X, the paths
will be "usb:" followed by hex digits.  For Windows, the device
paths will be printed as "????????????" and the -s option will
not be able to select a device until someone implements the
underlying functionality in usb_windows.c.

Change-Id: I1f01b8f47acd32edb0ac18db107316a2c923bbde
Signed-off-by: Scott Anderson <saa@android.com>
diff --git a/fastboot/engine.c b/fastboot/engine.c
index 345577d..89cff3f 100644
--- a/fastboot/engine.c
+++ b/fastboot/engine.c
@@ -480,6 +480,8 @@
     int status = 0;
 
     a = action_list;
+    if (!a)
+        return status;
     resp[FB_RESPONSE_SZ] = 0;
 
     double start = -1;
@@ -516,3 +518,8 @@
     fprintf(stderr,"finished. total time: %.3fs\n", (now() - start));
     return status;
 }
+
+int fb_queue_is_empty(void)
+{
+    return (action_list == NULL);
+}
diff --git a/fastboot/fastboot.c b/fastboot/fastboot.c
index 805014f..f1b675f 100644
--- a/fastboot/fastboot.c
+++ b/fastboot/fastboot.c
@@ -58,6 +58,7 @@
 static const char *cmdline = 0;
 static int wipe_data = 0;
 static unsigned short vendor_id = 0;
+static int long_listing = 0;
 
 static unsigned base_addr = 0x10000000;
 
@@ -163,9 +164,10 @@
     if(info->ifc_class != 0xff) return -1;
     if(info->ifc_subclass != 0x42) return -1;
     if(info->ifc_protocol != 0x03) return -1;
-    // require matching serial number if a serial number is specified
+    // require matching serial number or device path if requested
     // at the command line with the -s option.
-    if (serial && strcmp(serial, info->serial_number) != 0) return -1;
+    if (serial && (strcmp(serial, info->serial_number) != 0 &&
+                   strcmp(serial, info->device_path) != 0)) return -1;
     return 0;
 }
 
@@ -179,8 +181,16 @@
         if (!serial[0]) {
             serial = "????????????";
         }
-        // output compatible with "adb devices"
-        printf("%s\tfastboot\n", serial);
+        if (!long_listing) {
+            // output compatible with "adb devices"
+            printf("%s\tfastboot\n", serial);
+        } else {
+            char* device_path = info->device_path;
+            if (!device_path[0]) {
+                device_path = "????????????";
+            }
+            printf("%s\t%s\tfastboot\n", serial, device_path);
+        }
     }
 
     return -1;
@@ -234,7 +244,9 @@
             "\n"
             "options:\n"
             "  -w                                       erase userdata and cache\n"
-            "  -s <serial number>                       specify device serial number\n"
+            "  -s <specific device>                     specify device serial number\n"
+            "                                           or path to device port\n"
+            "  -l                                       with \"devices\", lists device paths\n"
             "  -p <product>                             specify product name\n"
             "  -c <cmdline>                             override kernel commandline\n"
             "  -i <vendor id>                           specify a custom USB vendor id\n"
@@ -567,6 +579,7 @@
     int wants_wipe = 0;
     int wants_reboot = 0;
     int wants_reboot_bootloader = 0;
+    int wants_device_list = 0;
     void *data;
     unsigned sz;
     unsigned page_size = 2048;
@@ -578,11 +591,6 @@
         return 1;
     }
 
-    if (!strcmp(*argv, "devices")) {
-        list_devices();
-        return 0;
-    }
-
     if (!strcmp(*argv, "help")) {
         usage();
         return 0;
@@ -608,6 +616,9 @@
             require(2);
             serial = argv[1];
             skip(2);
+        } else if(!strcmp(*argv, "-l")) {
+            long_listing = 1;
+            skip(1);
         } else if(!strcmp(*argv, "-p")) {
             require(2);
             product = argv[1];
@@ -626,6 +637,9 @@
                 die("invalid vendor id '%s'", argv[1]);
             vendor_id = (unsigned short)val;
             skip(2);
+        } else if (!strcmp(*argv, "devices")) {
+            skip(1);
+            wants_device_list = 1;
         } else if(!strcmp(*argv, "getvar")) {
             require(2);
             fb_queue_display(argv[1], argv[1]);
@@ -721,6 +735,9 @@
         }
     }
 
+    if (wants_device_list)
+        list_devices();
+
     if (wants_wipe) {
         fb_queue_erase("userdata");
         fb_queue_format("userdata", 1);
@@ -733,6 +750,9 @@
         fb_queue_command("reboot-bootloader", "rebooting into bootloader");
     }
 
+    if (fb_queue_is_empty())
+        return 0;
+
     usb = open_device();
 
     status = fb_execute_queue(usb);
diff --git a/fastboot/fastboot.h b/fastboot/fastboot.h
index b77cb19..6d036f8 100644
--- a/fastboot/fastboot.h
+++ b/fastboot/fastboot.h
@@ -53,6 +53,7 @@
 void fb_queue_download(const char *name, void *data, unsigned size);
 void fb_queue_notice(const char *notice);
 int fb_execute_queue(usb_handle *usb);
+int fb_queue_is_empty(void);
 
 /* util stuff */
 void die(const char *fmt, ...);
diff --git a/fastboot/usb.h b/fastboot/usb.h
index cc157d5..7a7d55b 100644
--- a/fastboot/usb.h
+++ b/fastboot/usb.h
@@ -53,6 +53,7 @@
     unsigned char writable;
 
     char serial_number[256];
+    char device_path[256];
 };
   
 typedef int (*ifc_match_func)(usb_ifc_info *ifc);
diff --git a/fastboot/usb_linux.c b/fastboot/usb_linux.c
index 1ba87e6..19be51e 100644
--- a/fastboot/usb_linux.c
+++ b/fastboot/usb_linux.c
@@ -32,6 +32,7 @@
 #include <string.h>
 
 #include <sys/ioctl.h>
+#include <sys/stat.h>
 #include <sys/types.h>
 #include <dirent.h>
 #include <fcntl.h>
@@ -108,6 +109,9 @@
     unsigned i;
     unsigned e;
     
+    struct stat st;
+    int result;
+
     if(check(ptr, len, USB_DT_DEVICE, USB_DT_DEVICE_SIZE))
         return -1;
     dev = (void*) ptr;
@@ -132,7 +136,6 @@
     if (dev->iSerialNumber) {
         struct usbdevfs_ctrltransfer  ctrl;
         __u16 buffer[128];
-        int result;
 
         memset(buffer, 0, sizeof(buffer));
 
@@ -155,6 +158,42 @@
         }
     }
 
+    /* We need to get a path that represents a particular port on a particular
+     * hub.  We are passed an fd that was obtained by opening an entry under
+     * /dev/bus/usb.  Unfortunately, the names of those entries change each
+     * time devices are plugged and unplugged.  So how to get a repeatable
+     * path?  udevadm provided the inspiration.  We can get the major and
+     * minor of the device file, read the symlink that can be found here:
+     *   /sys/dev/char/<major>:<minor>
+     * and then use the last element of that path.  As a concrete example, I
+     * have an Android device at /dev/bus/usb/001/027 so working with bash:
+     *   $ ls -l /dev/bus/usb/001/027
+     *   crw-rw-r-- 1 root plugdev 189, 26 Apr  9 11:03 /dev/bus/usb/001/027
+     *   $ ls -l /sys/dev/char/189:26
+     *   lrwxrwxrwx 1 root root 0 Apr  9 11:03 /sys/dev/char/189:26 ->
+     *           ../../devices/pci0000:00/0000:00:1a.7/usb1/1-4/1-4.2/1-4.2.3
+     * So our device_path would be 1-4.2.3 which says my device is connected
+     * to port 3 of a hub on port 2 of a hub on port 4 of bus 1 (per
+     * http://www.linux-usb.org/FAQ.html).
+     */
+    info.device_path[0] = '\0';
+    result = fstat(fd, &st);
+    if (!result && S_ISCHR(st.st_mode)) {
+        char cdev[128];
+        char link[256];
+        char *slash;
+        ssize_t link_len;
+        snprintf(cdev, sizeof(cdev), "/sys/dev/char/%d:%d",
+                 major(st.st_rdev), minor(st.st_rdev));
+        link_len = readlink(cdev, link, sizeof(link) - 1);
+        if (link_len > 0) {
+            link[link_len] = '\0';
+            slash = strrchr(link, '/');
+            if (slash)
+                snprintf(info.device_path, sizeof(info.device_path), "usb:%s", slash+1);
+        }
+    }
+
     for(i = 0; i < cfg->bNumInterfaces; i++) {
         if(check(ptr, len, USB_DT_INTERFACE, USB_DT_INTERFACE_SIZE))
             return -1;
diff --git a/fastboot/usb_osx.c b/fastboot/usb_osx.c
index 570a456..605c8e2 100644
--- a/fastboot/usb_osx.c
+++ b/fastboot/usb_osx.c
@@ -264,6 +264,7 @@
     SInt32 score;
     HRESULT result;
     UInt8 serialIndex;
+    UInt32 locationId;
 
     // Create an intermediate plugin.
     kr = IOCreatePlugInInterfaceForService(device,
@@ -322,6 +323,13 @@
         goto error;
     }
 
+    kr = (*dev)->GetLocationID(dev, &locationId);
+    if (kr != 0) {
+        ERR("GetLocationId");
+        goto error;
+    }
+    snprintf(handle->info.device_path, sizeof(handle->info.device_path), "usb:%lX", locationId);
+
     kr = (*dev)->USBGetSerialNumberStringIndex(dev, &serialIndex);
 
     if (serialIndex > 0) {
diff --git a/fastboot/usb_windows.c b/fastboot/usb_windows.c
index 1050293..10317bf 100644
--- a/fastboot/usb_windows.c
+++ b/fastboot/usb_windows.c
@@ -311,6 +311,8 @@
         info.serial_number[0] = 0;
     }
 
+    info.device_path[0] = 0;
+
     if (callback(&info) == 0) {
         return 1;
     }