rbscsi: add experimental API to list connected SCSI devices

For now it is only implemented on linux using /sys scanning

Change-Id: Ifdfe7564e6e8d0307ae6ddc53e49bb9aaf5a8268
diff --git a/utils/scsi/rbscsi.c b/utils/scsi/rbscsi.c
index a43608a..8e15f1f 100644
--- a/utils/scsi/rbscsi.c
+++ b/utils/scsi/rbscsi.c
@@ -18,6 +18,8 @@
  * KIND, either express or implied.
  *
  ****************************************************************************/
+#define _XOPEN_SOURCE 500
+#define _DEFAULT_SOURCE
 #include <stdlib.h>
 #include <stdio.h>
 #include <stdarg.h>
@@ -47,6 +49,10 @@
 #include <errno.h>
 #include <sys/ioctl.h>
 #include <scsi/sg.h>
+#include <dirent.h>
+#include <linux/limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
 #define RB_SCSI_LINUX
 typedef int rb_scsi_handle_t;
 #else
@@ -151,7 +157,175 @@
     free(dev);
 }
 
-/* Windpws */
+static int is_hctl(const char *name)
+{
+    char *end;
+    strtoul(name, &end, 0); /* h */
+    if(*end != ':')
+        return 0;
+    strtoul(end + 1, &end, 0); /* c */
+    if(*end != ':')
+        return 0;
+    strtoul(end + 1, &end, 0); /* t */
+    if(*end != ':')
+        return 0;
+    strtoul(end + 1, &end, 0); /* l */
+    return *end == 0;
+}
+
+static int _resolve_link_dev_path(char *path, size_t pathsz)
+{
+    /* make sure it is a directory */
+    struct stat st;
+    if(stat(path, &st) < 0)
+        return 0;
+    if(!S_ISDIR(st.st_mode))
+        return 0;
+    if(chdir(path) < 0)
+        return 0;
+    if(getcwd(path, pathsz) == NULL)
+        return 0;
+    return 1;
+}
+
+static int resolve_link_dev_path(char *path, size_t pathsz)
+{
+    /* change directory, ask the current path and resolve current directory */
+    char curdir[PATH_MAX];
+    if(getcwd(curdir, sizeof(curdir)) == NULL)
+        return 0;
+    int ret = _resolve_link_dev_path(path, pathsz);
+    chdir(curdir);
+    return ret;
+}
+
+static int scan_resolve_first_dev_path(char *path, size_t pathsz)
+{
+    size_t pathlen = strlen(path);
+    char *pathend = path + pathlen;
+    DIR *dir = opendir(path);
+    if(dir == NULL)
+        return 0;
+    struct dirent *d;
+    int ret = 0;
+    while((d = readdir(dir)))
+    {
+        if(!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+            continue;
+        /* we found the entry, if it is a symlink, resolve it, otherwise it must be a directory */
+        if(d->d_type == DT_LNK)
+        {
+            snprintf(pathend, pathsz - pathlen, "/%s", d->d_name);
+            ret = resolve_link_dev_path(path, pathsz);
+        }
+        else if(d->d_type == DT_DIR)
+        {
+            snprintf(path, pathsz, "/dev/%s", d->d_name);
+            ret = 1;
+        }
+        break;
+    }
+    closedir(dir);
+    return ret;
+}
+
+static int scan_resolve_dev_path(char *path, size_t pathsz, const char *match)
+{
+    size_t pathlen = strlen(path);
+    char *pathend = path + pathlen;
+    DIR *dir = opendir(path);
+    if(dir == NULL)
+        return 0;
+    struct dirent *d;
+    int ret = 0;
+    while((d = readdir(dir)))
+    {
+        if(strcmp(d->d_name, match))
+            continue;
+        /* we found the entry, there are two case:
+         * - directory: we need to scan it and find the first entry
+         * - symlink: we need to see where it goes and extract the basename */
+        snprintf(pathend, pathsz - pathlen, "/%s", d->d_name);
+        if(d->d_type == DT_DIR)
+            ret = scan_resolve_first_dev_path(path, pathsz);
+        else if(d->d_type == DT_LNK)
+            ret = resolve_link_dev_path(path, pathsz);
+        break;
+    }
+    closedir(dir);
+    return ret;
+}
+
+static char *read_file_or_null(const char *path)
+{
+    FILE *f = fopen(path, "r");
+    if(f == NULL)
+        return NULL;
+    char buffer[1024];
+    if(fgets(buffer, sizeof(buffer), f) == NULL)
+    {
+        fclose(f);
+        return NULL;
+    }
+    fclose(f);
+    /* the kernel appends a '\n' at the end, remove it */
+    size_t len = strlen(buffer);
+    if(len > 0 && buffer[len - 1] == '\n')
+        buffer[len - 1] = 0;
+    return strdup(buffer);
+}
+
+struct rb_scsi_devent_t *rb_scsi_list(void)
+{
+    /* list devices in /sys/bus/scsi/devices
+     * we only keep entries of the form h:c:t:l */
+#define SYS_SCSI_DEV_PATH   "/sys/bus/scsi/devices"
+    DIR *dir = opendir(SYS_SCSI_DEV_PATH);
+    if(dir == NULL)
+        return NULL;
+    struct dirent *d;
+    struct rb_scsi_devent_t *dev = malloc(sizeof(struct rb_scsi_devent_t));
+    dev[0].scsi_path = NULL;
+    dev[0].block_path = NULL;
+    int nr_dev = 0;
+    while((d = readdir(dir)))
+    {
+        if(!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+            continue;
+        /* make sure the name is of the form h:c:t:l, we do not want targets or hosts */
+        if(!is_hctl(d->d_name))
+            continue;
+        /* we now need to scan the directory to find the block and scsi generic device path:
+         * block: there should be a 'block' entry
+         * scsi: there should be a 'scsi_generic' entry
+         * Both entries can either be a symlink to the devide, or a directory with a single entry */
+        char scsi_path[PATH_MAX];
+        snprintf(scsi_path, sizeof(scsi_path), SYS_SCSI_DEV_PATH "/%s", d->d_name);
+        if(!scan_resolve_dev_path(scsi_path, sizeof(scsi_path), "scsi_generic"))
+            continue;
+        char block_path[PATH_MAX];
+        snprintf(block_path, sizeof(block_path), SYS_SCSI_DEV_PATH "/%s", d->d_name);
+        if(!scan_resolve_dev_path(block_path, sizeof(block_path), "block"))
+            block_path[0] = 0;
+        dev = realloc(dev, (2 + nr_dev) * sizeof(struct rb_scsi_devent_t));
+        dev[nr_dev].scsi_path = strdup(scsi_path);
+        dev[nr_dev].block_path = block_path[0] == 0 ? NULL : strdup(block_path);
+        /* fill vendor/model/rev */
+        snprintf(scsi_path, sizeof(scsi_path), SYS_SCSI_DEV_PATH "/%s/vendor", d->d_name);
+        dev[nr_dev].vendor = read_file_or_null(scsi_path);
+        snprintf(scsi_path, sizeof(scsi_path), SYS_SCSI_DEV_PATH "/%s/model", d->d_name);
+        dev[nr_dev].model = read_file_or_null(scsi_path);
+        snprintf(scsi_path, sizeof(scsi_path), SYS_SCSI_DEV_PATH "/%s/rev", d->d_name);
+        dev[nr_dev].rev = read_file_or_null(scsi_path);
+
+        /* sentinel */
+        dev[++nr_dev].scsi_path = NULL;
+        dev[nr_dev].block_path = NULL;
+    }
+    closedir(dir);
+    return dev;
+}
+/* Windows */
 #elif defined(RB_SCSI_WINDOWS)
 /* return either path or something allocated with malloc() */
 static const char *map_to_physical_drive(const char *path, unsigned flags, void *user,
@@ -275,6 +449,14 @@
     free(dev);
 }
 
+struct rb_scsi_devent_t *rb_scsi_list(void)
+{
+    /* unimplemented */
+    struct rb_scsi_devent_t *dev = malloc(sizeof(struct rb_scsi_devent_t));
+    dev[0].scsi_path = NULL;
+    dev[0].block_path = NULL;
+    return dev;
+}
 /* other targets */
 #else
 rb_scsi_device_t rb_scsi_open(const char *path, unsigned flags, void *user,
@@ -297,6 +479,15 @@
 {
     free(dev);
 }
+
+struct rb_scsi_devent_t *rb_scsi_list(void)
+{
+    /* unimplemented */
+    struct rb_scsi_devent_t *dev = malloc(sizeof(struct rb_scsi_devent_t));
+    dev[0].scsi_path = NULL;
+    dev[0].block_path = NULL;
+    return dev;
+}
 #endif
 
 void rb_scsi_decode_sense(rb_scsi_device_t dev, void *_sense, int sense_len)
@@ -333,3 +524,22 @@
 
 #undef rb_printf
 }
+
+void rb_scsi_free_list(struct rb_scsi_devent_t *list)
+{
+    if(list == NULL)
+        return;
+    for(struct rb_scsi_devent_t *p = list; p->scsi_path; p++)
+    {
+        free(p->scsi_path);
+        if(p->block_path)
+            free(p->block_path);
+        if(p->vendor)
+            free(p->vendor);
+        if(p->model)
+            free(p->model);
+        if(p->rev)
+            free(p->rev);
+    }
+    free(list);
+}
diff --git a/utils/scsi/rbscsi.h b/utils/scsi/rbscsi.h
index c7345a6..322d94e 100644
--- a/utils/scsi/rbscsi.h
+++ b/utils/scsi/rbscsi.h
@@ -89,6 +89,31 @@
 /* close a device */
 void rb_scsi_close(rb_scsi_device_t dev);
 
+/* SCSI device reported by rb_scsi_list() */
+struct rb_scsi_devent_t
+{
+    /* device path to the raw SCSI device, typically:
+     * - Linux: /dev/sgX
+     * - Windows: TODO
+     * This path can be used directly with scsi_rb_open(), and is guaranteed to
+     * be valid. */
+    char *scsi_path;
+    /* device path to the corresponding block device, if it exists, typically:
+     * - Linux: /dev/sdX
+     * - Windows: TODO
+     * If this path is not-NULL, then it can used directly with scsi_rb_open() */
+    char *block_path;
+    /* various information about the device, can be NULL on error */
+    char *vendor;
+    char *model;
+    char *rev;
+};
+/* try to list all SCSI devices, returns a list of devices or NULL on error
+ * the list is terminated by an entry with scsi_path=NULL */
+struct rb_scsi_devent_t *rb_scsi_list(void);
+/* free the list returned by rb_scsi_list */
+void rb_scsi_free_list(struct rb_scsi_devent_t *list);
+
 #ifdef __cplusplus
 }
 #endif