From ea14d1132d000d3ee3ea9bfd81eba8f616480ad3 Mon Sep 17 00:00:00 2001 From: nirenjan Date: Thu, 27 Jul 2017 16:43:01 -0700 Subject: [PATCH] Add implementation of stub libusb library This commit adds the implementation of the stub libusb library in order to use it in an LD_PRELOAD environment. This also adds the utility programs to create a device list and sample output to compare against. --- .gitignore | 1 + docs/design/libusbx52.md | 35 +++++ libusbx52/Makefile.am | 12 +- libusbx52/libusbx52.h | 53 +++++++ libusbx52/usb_x52_stub.c | 205 ++++++++++++++++++++++++++-- libusbx52/util/create_device_list.c | 47 +++++++ libusbx52/util/log_actions.c | 97 +++++++++++++ 7 files changed, 436 insertions(+), 14 deletions(-) create mode 100644 docs/design/libusbx52.md create mode 100644 libusbx52/libusbx52.h create mode 100644 libusbx52/util/create_device_list.c create mode 100644 libusbx52/util/log_actions.c diff --git a/.gitignore b/.gitignore index 96d27c1..7a334a8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ cli/x52cli* test/x52test* util/util_char_map.c util/x52charmapgen* +libusbx52/x52test* x52pro-linux-*.tar.gz # Module files diff --git a/docs/design/libusbx52.md b/docs/design/libusbx52.md new file mode 100644 index 0000000..3dc56ce --- /dev/null +++ b/docs/design/libusbx52.md @@ -0,0 +1,35 @@ +LibUSB X52 stub library +======================= + +The libusbx52 stub library is a convenience library to help test the libusb +functions used by libx52 and associated code. It simulates the behavior of the +libusb functions used by the libx52 library, but doesn't actually control any +real hardware, and simply updates a few in-memory data structures. + +The idea behind `libusbx52.so` is to use it as an LD_PRELOAD library, where it +will override the real functions used by `libx52.so`. The use case for this +scenario is in an automated testing environment, where a test runner could set +up the list of devices manually and simulate various scenarios. + +# Design Overview + +Unfortunately, the automake infrastructure does not support the use of +LD_PRELOAD because it is deemed "non-portable" in the automake sense. As a +result, this is now up to a test runner application to implement a method to +control the data passed between two processes. + +# Data Structures + +The server process is responsible for setting up the initial set of USB devices. +As far as libx52 is concerned, the only fields that it uses in the USB +descriptor are the idVendor and idProduct fields. These are written to a file +that is read by the libusbx52 stubs to populate the device list. + +Once the file has been written by the server, the libusb_init stub function in +the client can read the file and populate the internal data structures as +necessary. + +The client can also write to a separate file to record the USB communication +sent across to the simulated device. + + diff --git a/libusbx52/Makefile.am b/libusbx52/Makefile.am index 16d0c19..bfd94c8 100644 --- a/libusbx52/Makefile.am +++ b/libusbx52/Makefile.am @@ -1,8 +1,18 @@ ACLOCAL_AMFLAGS = -I m4 # libusb stub library for use by test programs -check_LTLIBRARIES = libusbx52.la +noinst_LTLIBRARIES = libusbx52.la libusbx52_la_SOURCES = usb_x52_stub.c +libusbx52_la_LDFLAGS = -rpath /nowhere + +# Utility programs for use by tests +noinst_PROGRAMS = x52test_create_device_list x52test_log_actions + +x52test_create_device_list_SOURCES = util/create_device_list.c + +x52test_log_actions_SOURCES = util/log_actions.c +x52test_log_actions_CFLAGS = @X52_INCLUDE@ +x52test_log_actions_LDADD = libusbx52.la EXTRA_DIST = README.md diff --git a/libusbx52/libusbx52.h b/libusbx52/libusbx52.h new file mode 100644 index 0000000..ed69519 --- /dev/null +++ b/libusbx52/libusbx52.h @@ -0,0 +1,53 @@ +/* + * LibUSB stub driver for testing the Saitek X52/X52 Pro + * + * Copyright (C) 2017 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, version 2. + * + */ + +#include +#include + +struct libusb_device { + struct libusb_context *context; + int index; + int ref_count; + struct libusb_device_descriptor desc; +}; + +struct libusb_context { + int block_size; // Set to LIBUSBX52_MEMORY_BLOCK_SIZE + int max_devices; // Calculated based on block_size + int debug_level; + int num_devices; + struct libusb_device *devices; +}; + +struct libusb_device_handle { + struct libusb_context *ctx; + struct libusb_device *dev; + int packets_written; + FILE *packet_data_file; +}; + +/** + * @brief File location of the device list file + * + * This file contains a list of VIDs and PIDs in hexadecimal format separated + * by spaces. There must be an even number of entries, each pair corresponding + * to a (VID, PID) tuple identifying a single USB device. + */ +#define INPUT_DEVICE_LIST_FILE "/tmp/libusbx52_device_list" + +/** + * @brief File location of the communication data file + * + * This file contains the libusb APIs called by libx52, after a device has + * been opened, i.e., all APIs that operate on a libusb_device_handle + */ +#define OUTPUT_DATA_FILE "/tmp/libusbx52_output_data" + diff --git a/libusbx52/usb_x52_stub.c b/libusbx52/usb_x52_stub.c index 6b7b6bf..820cbf4 100644 --- a/libusbx52/usb_x52_stub.c +++ b/libusbx52/usb_x52_stub.c @@ -9,52 +9,219 @@ * */ +#include +#include #include - +#include "libusbx52.h" int libusb_init(libusb_context **ctx) { - // TODO - return 0; + int rc; + int dev_count; + int vid; + int pid; + int parsed; + FILE *dev_list; + + /* + * Technically, libusb_init can be called with a NULL context pointer, + * in which case, libusb will allocate a default context. However, in + * the case of libx52, the libusb APIs are always called with a non-NULL + * context pointer, which is then initialized with the allocated context + * pointer. + */ + libusb_context *tmp_ctx = calloc(1, sizeof(*tmp_ctx)); + if (tmp_ctx == NULL) { + rc = LIBUSB_ERROR_NO_MEM; + goto init_err_recovery; + } + + dev_list = fopen(INPUT_DEVICE_LIST_FILE, "r"); + if (dev_list == NULL) { + rc = LIBUSB_ERROR_IO; + goto init_err_recovery; + } + + /* Determine the number of devices in the file */ + dev_count = 0; + do { + parsed = fscanf(dev_list, "%x %x", &vid, &pid); + /* + * If we have read fewer than 2 items, then quit. + * We've read all we can + */ + if (parsed < 2) { + break; + } + dev_count++; + } while (!feof(dev_list)); + + /* Make sure we have at least 1 device */ + if (dev_count == 0) { + rc = LIBUSB_ERROR_NOT_FOUND; + goto init_err_recovery; + } + + /* We now have the number of devices, allocate memory for them */ + tmp_ctx->devices = calloc(dev_count, sizeof(*(tmp_ctx->devices))); + if (tmp_ctx->devices == NULL) { + rc = LIBUSB_ERROR_NO_MEM; + goto init_err_recovery; + } + tmp_ctx->num_devices = dev_count; + + /* Rewind and read the file again, but now put them into the device list */ + rewind(dev_list); + + for (int i = 0; i < dev_count && !feof(dev_list); i++) { + /* Set the base fields */ + tmp_ctx->devices[i].context = tmp_ctx; + tmp_ctx->devices[i].index = i; + + parsed = fscanf(dev_list, "%x %x", &vid, &pid); + if (parsed < 2) { + /* Parse error, skip this device */ + continue; + } + /* Set the VID & PID */ + tmp_ctx->devices[i].desc.idVendor = vid; + tmp_ctx->devices[i].desc.idProduct = pid; + } + + /* Done, close the file and return */ + fclose(dev_list); + + *ctx = tmp_ctx; + return LIBUSB_SUCCESS; + +init_err_recovery: + /* Close the device list file if it is open */ + if (dev_list) { + fclose(dev_list); + } + + /* Free up any allocated memory */ + libusb_exit(tmp_ctx); + + return rc; } void libusb_exit(libusb_context *ctx) { - // TODO + if (ctx) { + if (ctx->devices) { + free(ctx->devices); + } + free(ctx); + } } void libusb_set_debug(libusb_context *ctx, int level) { - // TODO + /* Set the debug level appropriately */ + ctx->debug_level = level; } ssize_t libusb_get_device_list(libusb_context *ctx, libusb_device ***list) { - // TODO - return 0; + /* Allocate a list of num_devices, each pointing to a corresponding + * libusb_device structure. + * + * Actually, the allocation adds one more than needed, since the list + * is supposed to be NULL terminated, according to the libusb docs. + */ + libusb_device **tmp_list = calloc(ctx->num_devices + 1, sizeof(*tmp_list)); + libusb_device *dev; + + if (tmp_list == NULL) { + return LIBUSB_ERROR_NO_MEM; + } + + /* Initialize the list with pointers to the individual devices */ + for (int i = 0; i < ctx->num_devices; i++) { + dev = &(ctx->devices[i]); + /* Increment the refcount */ + dev->ref_count += 1; + tmp_list[i] = dev; + } + + *list = tmp_list; + return ctx->num_devices; } void libusb_free_device_list(libusb_device **list, int unref_devices) { - // TODO + if (unref_devices) { + for (libusb_device **dev = list; *dev; dev++) { + /* Decrement the refcount */ + (*dev)->ref_count -= 1; + } + } + + free(list); } int libusb_get_device_descriptor(libusb_device *dev, struct libusb_device_descriptor *desc) { - // TODO + /* Copy the descriptor to the destination address */ + *desc = dev->desc; return 0; } +#define LIBUSB_DUMP_LOG_FILE(hdl, loglevel, fmt_str, ...) do { \ + if (hdl->ctx->debug_level != LIBUSB_LOG_LEVEL_NONE && \ + hdl->ctx->debug_level >= loglevel) { \ + fprintf(hdl->packet_data_file, "%s: " fmt_str, __func__, __VA_ARGS__); \ + } \ +} while (0) + int libusb_open(libusb_device *dev, libusb_device_handle **handle) { - // TODO - return 0; + /* Allocate a handle for the application */ + libusb_device_handle *tmp_hdl = calloc(1, sizeof(*tmp_hdl)); + if (tmp_hdl == NULL) { + return LIBUSB_ERROR_NO_MEM; + } + + /* Increment the reference count for the underlying device */ + dev->ref_count += 1; + + /* Populate the handle structure with the right values */ + tmp_hdl->ctx = dev->context; + tmp_hdl->dev = dev; + tmp_hdl->packet_data_file = fopen(OUTPUT_DATA_FILE, "w"); + + /* Make sure that the file opened correctly */ + if (tmp_hdl->packet_data_file == NULL) { + /* Error condition, return with a failure */ + free(tmp_hdl); + return LIBUSB_ERROR_IO; + } + + LIBUSB_DUMP_LOG_FILE(tmp_hdl, LIBUSB_LOG_LEVEL_DEBUG, + "VID: %04x PID: %04x idx: %d ref: %d\n", + dev->desc.idVendor, dev->desc.idProduct, dev->index, dev->ref_count); + + *handle = tmp_hdl; + return LIBUSB_SUCCESS; } void libusb_close(libusb_device_handle *dev_handle) { - // TODO + /* Decrement the refcount for the underlying device */ + dev_handle->dev->ref_count -= 1; + + libusb_device *dev = dev_handle->dev; + LIBUSB_DUMP_LOG_FILE(dev_handle, LIBUSB_LOG_LEVEL_DEBUG, + "VID: %04x PID: %04x idx: %d ref: %d\n", + dev->desc.idVendor, dev->desc.idProduct, dev->index, dev->ref_count); + + /* Close the file */ + fclose(dev_handle->packet_data_file); + + /* Free any memory used */ + free(dev_handle); } int libusb_control_transfer(libusb_device_handle *dev_handle, @@ -66,6 +233,18 @@ int libusb_control_transfer(libusb_device_handle *dev_handle, uint16_t wLength, unsigned int timeout) { - // TODO + /* Always log the control transfer */ + fprintf(dev_handle->packet_data_file, + "%s: RqType: %02x bRequest: %02x wValue: %04x wIndex: %04x timeout: %d\n", + __func__, request_type, bRequest, wValue, wIndex, timeout); + if (data != NULL) { + fprintf(dev_handle->packet_data_file, "%s: Data[%d]: ", __func__, + wLength); + for (int i = 0; i < wLength; i++) { + fprintf(dev_handle->packet_data_file, "%02x ", data[i]); + } + fprintf(dev_handle->packet_data_file, "\n"); + } + return 0; } diff --git a/libusbx52/util/create_device_list.c b/libusbx52/util/create_device_list.c new file mode 100644 index 0000000..502bae0 --- /dev/null +++ b/libusbx52/util/create_device_list.c @@ -0,0 +1,47 @@ +/* + * LibUSB test utility library + * + * This program generates a list of USB devices for use by libusbx52.so + * + * Copyright (C) Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, version 2. + * + */ + +#include +#include +#include +#include "libusbx52.h" + +int main(int argc, char *argv[]) +{ + FILE *data; + char **id_pair; + int vid; + int pid; + int parsed; + + data = fopen(INPUT_DEVICE_LIST_FILE, "w"); + if (data == NULL) { + fprintf(stderr, "Unable to open %s for writing\n", + INPUT_DEVICE_LIST_FILE); + fprintf(stderr, "%s\n", strerror(errno)); + } + + /* Process arguments until there are fewer than 2 remaining */ + for (int i = 1; i < argc && (argc - i) >= 2; i += 2) { + parsed = sscanf(argv[i], "%x", &vid); + if (parsed != 1) break; + + parsed = sscanf(argv[i+1], "%x", &pid); + if (parsed != 1) break; + + fprintf(data, "%04x %04x\n", vid, pid); + } + + fclose(data); + return 0; +} diff --git a/libusbx52/util/log_actions.c b/libusbx52/util/log_actions.c new file mode 100644 index 0000000..4531cf9 --- /dev/null +++ b/libusbx52/util/log_actions.c @@ -0,0 +1,97 @@ +/* + * LibUSB test utility library + * + * This program calls the libusb_control_transfer API to log the bytes to + * the given file. This is useful to generate a file with the same syntax + * and verify that they match. + * + * Copyright (C) Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, version 2. + * + */ + +#include +#include +#include +#include +#include "libusbx52.h" +#include "x52_commands.h" + +libusb_context *global_context; + +static int send_command(libusb_device_handle *hdl, uint16_t index, uint16_t value) +{ + return libusb_control_transfer(hdl, + LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE | LIBUSB_ENDPOINT_OUT, + X52_VENDOR_REQUEST, value, index, NULL, 0, 5000); +} + +static libusb_device_handle *libusbx52_init(void) +{ + int rc; + ssize_t count; + int i; + libusb_device **list; + libusb_device *device; + libusb_device_handle *hdl = NULL; + struct libusb_device_descriptor desc; + + rc = libusb_init(&global_context); + if (rc) { + return NULL; + } + + libusb_set_debug(global_context, LIBUSB_LOG_LEVEL_ERROR); + + count = libusb_get_device_list(global_context, &list); + for (i = 0; i < count; i++) { + device = list[i]; + if (!libusb_get_device_descriptor(device, &desc)) { + if (desc.idVendor == 0x06a3) { + if (desc.idProduct == 0x0762) { + rc = libusb_open(device, &hdl); + if (rc) { + if (hdl) free(hdl); + hdl = NULL; + break; + } + } + } + } + } + + libusb_free_device_list(list, 1); + + return hdl; +} + +int main(int argc, char *argv[]) +{ + int index; + int value; + int parsed; + libusb_device_handle *hdl; + libusb_context *ctx; + + hdl = libusbx52_init(); + ctx = hdl->ctx; + + /* Process arguments until there are fewer than 2 remaining */ + for (int i = 1; i < argc && (argc - i) >= 2; i += 2) { + parsed = sscanf(argv[i], "%x", &index); + if (parsed != 1) break; + + parsed = sscanf(argv[i+1], "%x", &value); + if (parsed != 1) break; + + send_command(hdl, index, value); + } + + libusb_close(hdl); + libusb_exit(ctx); + + return 0; +}