[Spice-devel] [PATCH linux vdagent 01/10] Add lookup_xrand_output_for_device_info()

Jonathon Jongsma jjongsma at redhat.com
Thu Dec 13 22:46:23 UTC 2018


Add a function to look up an xrandr output for a given device display
id. This uses sysfs and the drm subsystem to lookup information about a
graphics device output. It then compares the drm output name to xrandr
output names to try to match that device output to an xrandr output.
This is necesary for guests that have multiple graphics devices.
---
 Makefile.am               |  14 ++
 configure.ac              |   1 +
 src/vdagent/device-info.c | 516 ++++++++++++++++++++++++++++++++++++++
 src/vdagent/device-info.h |  26 ++
 tests/test-device-info.c  | 262 +++++++++++++++++++
 5 files changed, 819 insertions(+)
 create mode 100644 src/vdagent/device-info.c
 create mode 100644 src/vdagent/device-info.h
 create mode 100644 tests/test-device-info.c

diff --git a/Makefile.am b/Makefile.am
index 3e405bc..d2f2258 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -12,6 +12,7 @@ common_sources =				\
 	$(NULL)
 
 src_spice_vdagent_CFLAGS =			\
+	$(DRM_CFLAGS)				\
 	$(X_CFLAGS)				\
 	$(SPICE_CFLAGS)				\
 	$(GLIB2_CFLAGS)				\
@@ -22,6 +23,7 @@ src_spice_vdagent_CFLAGS =			\
 	$(NULL)
 
 src_spice_vdagent_LDADD =			\
+	$(DRM_LIBS)				\
 	$(X_LIBS)				\
 	$(SPICE_LIBS)				\
 	$(GLIB2_LIBS)				\
@@ -35,6 +37,8 @@ src_spice_vdagent_SOURCES =			\
 	src/vdagent/audio.h			\
 	src/vdagent/clipboard.c			\
 	src/vdagent/clipboard.h			\
+	src/vdagent/device-info.c		\
+	src/vdagent/device-info.h		\
 	src/vdagent/file-xfers.c		\
 	src/vdagent/file-xfers.h		\
 	src/vdagent/x11-priv.h			\
@@ -137,3 +141,13 @@ EXTRA_DIST =					\
 DISTCHECK_CONFIGURE_FLAGS =			\
 	--with-init-script=redhat		\
 	$(NULL)
+
+tests_test_device_info_LDADD = $(src_spice_vdagent_LDADD)
+tests_test_device_info_CFLAGS = $(src_spice_vdagent_CFLAGS)
+
+check_PROGRAMS = 					\
+	tests/test-device-info			\
+	$(NULL)
+
+TESTS = $(check_PROGRAMS)
+
diff --git a/configure.ac b/configure.ac
index 7cb44db..55b031e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -105,6 +105,7 @@ PKG_CHECK_MODULES(X, [xfixes xrandr >= 1.3 xinerama x11])
 PKG_CHECK_MODULES(SPICE, [spice-protocol >= 0.12.13])
 PKG_CHECK_MODULES(ALSA, [alsa >= 1.0.22])
 PKG_CHECK_MODULES([DBUS], [dbus-1])
+PKG_CHECK_MODULES([DRM], [libdrm])
 
 if test "$with_session_info" = "auto" || test "$with_session_info" = "systemd"; then
     PKG_CHECK_MODULES([LIBSYSTEMD_LOGIN],
diff --git a/src/vdagent/device-info.c b/src/vdagent/device-info.c
new file mode 100644
index 0000000..c519741
--- /dev/null
+++ b/src/vdagent/device-info.c
@@ -0,0 +1,516 @@
+/*  device-info.c utility function for looking up the xrandr output id for a
+ *  given device address and display id
+ *
+ * Copyright 2018 Red Hat, Inc.
+ *
+ * Red Hat Authors:
+ * Jonathon Jongsma <jjongsma at redhat.com>
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <syslog.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+#include <unistd.h>
+#include <X11/extensions/Xrandr.h>
+#include <glib.h>
+
+#include "device-info.h"
+
+typedef struct PciDevice {
+    int domain;
+    uint8_t bus;
+    uint8_t slot;
+    uint8_t function;
+} PciDevice;
+
+typedef struct PciAddress {
+    int domain;
+    GList *devices; /* PciDevice */
+} PciAddress;
+
+static PciAddress* pci_address_new()
+{
+    return g_new0(PciAddress, 1);
+}
+
+static void pci_address_free(PciAddress *addr)
+{
+    g_list_free_full(addr->devices, g_free);
+    g_free(addr);
+}
+
+
+static int read_next_hex_number(const char *input, char delim, char **endptr)
+{
+    assert(input != NULL);
+    assert(endptr != NULL);
+
+    const char *pos = strchr(input, delim);
+    int n;
+    if (!pos) {
+        *endptr = NULL;
+        return 0;
+    }
+
+    char *endpos;
+    n = strtol(input, &endpos, 16);
+
+    // check if we read all characters until the delimiter
+    if (endpos != pos)
+        endpos = NULL;
+
+    *endptr = endpos;
+    return n;
+}
+
+// the device should be specified in BDF notation (e.g. 0000:00:02.0)
+// see https://wiki.xen.org/wiki/Bus:Device.Function_(BDF)_Notation
+static bool parse_pci_device(const char *bdf, const char *end, PciDevice *device)
+{
+    if (!end) end = strchr(bdf, 0);
+
+    int endpos = -1;
+    int domain, bus, slot, function;
+    sscanf(bdf, "%x:%x:%x.%x%n", &domain, &bus, &slot, &function, &endpos);
+    if (!device || endpos < 0 || bdf + endpos != end)
+        return false;
+    if (domain < 0 || bus < 0 || slot < 0 || function < 0)
+        return false;
+
+    device->domain = domain;
+    device->bus = bus;
+    device->slot = slot;
+    device->function = function;
+    return true;
+}
+
+// We need to extract the pci address of the device from the sysfs entry for the device like so:
+// $ readlink /sys/class/drm/card0
+// This should give you a path such as this for cards on the root bus:
+// /sys/devices/pci0000:00/0000:00:02.0/drm/card0
+// or something like this if there is a pci bridge:
+// /sys/devices/pci0000:00/0000:00:03.0/0000:01:01.0/0000:02:03.0/virtio2/drm/card0
+static PciAddress* parse_pci_address_from_sysfs_path(const char* addr)
+{
+    char *pos = strstr(addr, "/pci");
+    if (!pos)
+        return NULL;
+
+    // advance to the numbers in pci0000:00
+    pos += 4;
+    int domain = read_next_hex_number(pos, ':', &pos);
+    if (!pos) {
+        return NULL;
+    }
+
+    // not used right now.
+    G_GNUC_UNUSED uint8_t bus = read_next_hex_number(pos + 1, '/', &pos);
+    if (!pos) {
+        return NULL;
+    }
+
+    PciAddress *address = pci_address_new();
+    address->domain = domain;
+    // now read all of the devices
+    while (pos) {
+        PciDevice *dev = g_new0(PciDevice, 1);
+        char *next = strchr(pos + 1, '/');
+        if (!parse_pci_device(pos + 1, next, dev)) {
+            g_free(dev);
+            break;
+        }
+        address->devices = g_list_append(address->devices, dev);
+        pos = next;
+    }
+    return address;
+}
+
+// format should be something like pci/$domain/$slot.$fn/$slot.$fn
+static PciAddress* parse_pci_address_from_spice(char *input)
+{
+    static const char prefix[] = "pci/";
+    if (strncmp(input, prefix, strlen(prefix)) != 0)
+        return NULL;
+
+    char *pos = input + strlen(prefix);
+    int domain = read_next_hex_number(pos, '/', &pos);
+    if (!pos) {
+        return NULL;
+    }
+
+    PciAddress *address = pci_address_new();
+    address->domain = domain;
+    // now read all of the devices
+    for (int n = 0; ; n++) {
+        PciDevice *dev = g_new0(PciDevice, 1);
+        char *next = strchr(pos + 1, '/');
+
+        dev->slot = read_next_hex_number(pos + 1, '.', &pos);
+        if (!pos) {
+            g_free(dev);
+            break;
+        }
+
+        dev->function = strtol(pos + 1, &pos, 16);
+        if (!pos || (next != NULL && next != pos)) {
+            g_free(dev);
+            break;
+        }
+
+        address->devices = g_list_append(address->devices, dev);
+        pos = next;
+        if (!pos)
+            break;
+    }
+    return address;
+}
+
+static bool compare_addresses(PciAddress *a, PciAddress *b)
+{
+    // only check domain, slot, and function
+    if (!(a->domain == b->domain
+        && g_list_length(a->devices) == g_list_length(b->devices))) {
+        return false;
+    }
+
+    for (GList *la = a->devices, *lb = b->devices;
+         la != NULL;
+         la = la->next, lb = lb->next) {
+        PciDevice *deva = la->data;
+        PciDevice *devb = lb->data;
+
+        if (deva->slot != devb->slot
+            || deva->function != devb->function) {
+            return false;
+        }
+    }
+    return true;
+}
+
+// Connector type names from xorg modesetting driver
+static const char * const modesetting_output_names[] = {
+    [DRM_MODE_CONNECTOR_Unknown] = "None" ,
+    [DRM_MODE_CONNECTOR_VGA] = "VGA" ,
+    [DRM_MODE_CONNECTOR_DVII] = "DVI-I" ,
+    [DRM_MODE_CONNECTOR_DVID] = "DVI-D" ,
+    [DRM_MODE_CONNECTOR_DVIA] = "DVI-A" ,
+    [DRM_MODE_CONNECTOR_Composite] = "Composite" ,
+    [DRM_MODE_CONNECTOR_SVIDEO] = "SVIDEO" ,
+    [DRM_MODE_CONNECTOR_LVDS] = "LVDS" ,
+    [DRM_MODE_CONNECTOR_Component] = "Component" ,
+    [DRM_MODE_CONNECTOR_9PinDIN] = "DIN" ,
+    [DRM_MODE_CONNECTOR_DisplayPort] = "DP" ,
+    [DRM_MODE_CONNECTOR_HDMIA] = "HDMI" ,
+    [DRM_MODE_CONNECTOR_HDMIB] = "HDMI-B" ,
+    [DRM_MODE_CONNECTOR_TV] = "TV" ,
+    [DRM_MODE_CONNECTOR_eDP] = "eDP" ,
+    [DRM_MODE_CONNECTOR_VIRTUAL] = "Virtual" ,
+    [DRM_MODE_CONNECTOR_DSI] = "DSI" ,
+    [DRM_MODE_CONNECTOR_DPI] = "DPI" ,
+};
+// Connector type names from qxl driver
+static const char * const qxl_output_names[] = {
+    [DRM_MODE_CONNECTOR_Unknown] = "None" ,
+    [DRM_MODE_CONNECTOR_VGA] = "VGA" ,
+    [DRM_MODE_CONNECTOR_DVII] = "DVI" ,
+    [DRM_MODE_CONNECTOR_DVID] = "DVI" ,
+    [DRM_MODE_CONNECTOR_DVIA] = "DVI" ,
+    [DRM_MODE_CONNECTOR_Composite] = "Composite" ,
+    [DRM_MODE_CONNECTOR_SVIDEO] = "S-video" ,
+    [DRM_MODE_CONNECTOR_LVDS] = "LVDS" ,
+    [DRM_MODE_CONNECTOR_Component] = "CTV" ,
+    [DRM_MODE_CONNECTOR_9PinDIN] = "DIN" ,
+    [DRM_MODE_CONNECTOR_DisplayPort] = "DisplayPort" ,
+    [DRM_MODE_CONNECTOR_HDMIA] = "HDMI" ,
+    [DRM_MODE_CONNECTOR_HDMIB] = "HDMI" ,
+    [DRM_MODE_CONNECTOR_TV] = "TV" ,
+    [DRM_MODE_CONNECTOR_eDP] = "eDP" ,
+    [DRM_MODE_CONNECTOR_VIRTUAL] = "Virtual" ,
+};
+
+
+static void drm_conn_name_full(drmModeConnector *conn, const char * const *names, int nnames, char *dest, size_t dlen, bool decrement_id)
+{
+    const char *type;
+
+    if (conn->connector_type_id < nnames &&
+        names[conn->connector_type]) {
+        type = names[conn->connector_type];
+    } else {
+        type = "unknown";
+    }
+
+    uint8_t id = conn->connector_type_id;
+    if (decrement_id) {
+        id--;
+    }
+    snprintf(dest, dlen, "%s-%d", type, id);
+}
+
+static void drm_conn_name_qxl(drmModeConnector *conn, char *dest, size_t dlen, bool decrement_id)
+{
+    return drm_conn_name_full(conn, qxl_output_names,
+                              sizeof(qxl_output_names)/sizeof(qxl_output_names[0]),
+                              dest, dlen, decrement_id);
+}
+
+// FIXME: there are some cases (for example, in my Lenovo T460p laptop with
+// intel graphics) where the modesetting driver uses a name such as DP-3-1
+// instead of DP-4. These outputs are not likely to be common in virtual
+// machines, so it may not matter much (?)
+static void drm_conn_name_modesetting(drmModeConnector *conn, char *dest, size_t dlen)
+{
+    return drm_conn_name_full(conn, modesetting_output_names,
+                              sizeof(modesetting_output_names)/sizeof(modesetting_output_names[0]),
+                              dest, dlen, false);
+}
+
+static const char *const connections[] = {"", "*CONNECTED*", "disconnected", "unknown connection"};
+
+#define PCI_VENDOR_ID_REDHAT 0x1b36
+#define PCI_VENDOR_ID_REDHAT_QUMRANET 0x1af4 // virtio-gpu
+#define PCI_VENDOR_ID_INTEL 0x8086
+#define PCI_VENDOR_ID_NVIDIA 0x10de
+
+#define PCI_DEVICE_ID_QXL 0x0100
+#define PCI_DEVICE_ID_VIRTIO_GPU 0x1050
+
+static bool read_hex_value_from_file(const char *path, int* value)
+{
+    if (value == NULL || path == NULL)
+        return false;
+
+    FILE *f = fopen(path, "r");
+    if (f == NULL)
+        return false;
+
+    int endpos = -1;
+    bool result = (fscanf(f, "%x\n%n", value, &endpos) > 0 && endpos >= 0);
+
+    fclose(f);
+    return result;
+}
+
+    // PCI address should be in the following format:
+    //   pci/$domain/$slot.$fn/$slot.$fn
+bool lookup_xrandr_output_for_device_info(VDAgentGraphicsDeviceInfo *device_info, RROutput *output_id)
+{
+    bool found_device = false;
+    XRRScreenResources *xres = NULL;
+    Display *xdisplay = NULL;
+    PciAddress *user_pci_addr = parse_pci_address_from_spice((char*)device_info->device_address);
+    if (!user_pci_addr) {
+        syslog(LOG_WARNING, "Couldn't parse PCI address '%s'. Address should be the form 'pci/$domain/$slot.$fn/$slot.fn...", device_info->device_address);
+        goto cleanup;
+    }
+    // look up xrandr outputs
+    xdisplay = XOpenDisplay(NULL);
+    if (!xdisplay) {
+        syslog(LOG_WARNING, "Failed to open X dislay");
+        goto cleanup;
+    }
+
+    int rr_event_base, rr_error_base;
+    if (!XRRQueryExtension(xdisplay, &rr_event_base, &rr_error_base)) {
+        syslog(LOG_WARNING, "Failed to initialize XRandr extension");
+        goto cleanup;
+    }
+
+    xres = XRRGetScreenResourcesCurrent(xdisplay, DefaultRootWindow(xdisplay));
+    if (!xres) {
+        syslog(LOG_WARNING, "Unable to get Xorg screen resources");
+        goto cleanup;
+    }
+
+    // Look for a device that matches the PCI address parsed above. Loop
+    // through the list of cards reported by the DRM subsytem
+    for (int i = 0; i < 10; ++i) {
+        char dev_path[64];
+        struct stat buf;
+
+        // device node for the card is needed to access libdrm functionality
+        snprintf(dev_path, sizeof(dev_path), DRM_DEV_NAME, DRM_DIR_NAME, i);
+        if (stat(dev_path, &buf) != 0) {
+            // no card exists, exit loop
+            syslog(LOG_DEBUG, "No card %i exists\n", i);
+            break;
+        }
+
+        // the sysfs directory for the card will allow us to determine the
+        // pci address for the device
+        char sys_path[64];
+        snprintf(sys_path, sizeof(sys_path), "/sys/class/drm/card%d", i);
+        syslog(LOG_DEBUG, "sys path: %s\n", sys_path);
+
+        // the file /sys/class/drm/card0 is a symlink to a file that
+        // specifies the device's address. It usually points to something
+        // like /sys/devices/pci0000:00/0000:00:02.0/drm/card0
+        char device_link[PATH_MAX];
+        if (realpath(sys_path, device_link) == NULL) {
+            syslog(LOG_WARNING, "Failed to get the real path of %s", sys_path);
+            goto cleanup;
+        }
+        syslog(LOG_DEBUG, "Device %s is at %s\n", dev_path, device_link);
+
+        PciAddress *drm_pci_addr = parse_pci_address_from_sysfs_path(device_link);
+        if (!drm_pci_addr) {
+            syslog(LOG_DEBUG, "Can't determine pci address from '%s'", device_link);
+            continue;
+        }
+
+        if (!compare_addresses(user_pci_addr, drm_pci_addr)) {
+            pci_address_free(drm_pci_addr);
+            syslog(LOG_DEBUG, "card addr '%s' does not match requested addr '%s'\n", device_link, device_info->device_address);
+            continue;
+        }
+        pci_address_free(drm_pci_addr);
+
+        bool found_output = false;
+        char id_path[150];
+        snprintf(id_path, sizeof(id_path), "%s/device/vendor", sys_path);
+        int vendor_id = 0;
+        if (!read_hex_value_from_file(id_path, &vendor_id)) {
+            syslog(LOG_WARNING, "Unable to read vendor ID of card\n");
+        } else {
+            syslog(LOG_DEBUG, "Vendor id of this card is %#x\n", vendor_id);
+        }
+        snprintf(id_path, sizeof(id_path), "%s/device/device", sys_path);
+        int device_id = 0;
+        if (!read_hex_value_from_file(id_path, &device_id)) {
+            syslog(LOG_WARNING, "Unable to read device ID of card\n");
+        } else {
+            syslog(LOG_DEBUG, "Device id of this card is %#x\n", device_id);
+        }
+
+        int drm_fd = open(dev_path, O_RDWR);
+        if (drm_fd < 0) {
+            syslog(LOG_WARNING, "Unable to open file %s", dev_path);
+            goto cleanup;
+        }
+
+        drmModeResPtr res = drmModeGetResources(drm_fd);
+        if (res) {
+            // find the drm output that is equal to device_display_id
+            if (device_info->device_display_id >= res->count_connectors) {
+                syslog(LOG_WARNING, "Specified display id %i is higher than the maximum display id provided by this device (%i)",
+                     device_info->device_display_id, res->count_connectors - 1);
+                goto cleanup;
+            }
+
+            drmModeConnectorPtr conn = drmModeGetConnector(drm_fd, res->connectors[device_info->device_display_id]);
+            drmModeFreeResources(res);
+            res = NULL;
+
+            bool decrement_name = false;
+            if (vendor_id == PCI_VENDOR_ID_REDHAT && device_id == PCI_DEVICE_ID_QXL) {
+                // Older QXL drivers numbered their outputs starting with
+                // 0. This contrasts with most drivers who start numbering
+                // outputs with 1.  In this case, the expected drm connector
+                // name will need to be decremented before comparing to the
+                // xrandr output name
+                for (int i = 0; i < xres->noutput; ++i) {
+                    XRROutputInfo *oinfo = XRRGetOutputInfo(xdisplay, xres, xres->outputs[i]);
+                    // the QXL device doesn't use the drm names directly, but
+                    // starts numbering from 0 instead of 1, so we need to
+                    // detect this condition and add 1 to all output names in
+                    // this scenario before comparing
+                    // check if one output is named "Virtual-0"
+                    if (strcmp(oinfo->name, "Virtual-0") == 0) {
+                        syslog(LOG_DEBUG, "Need to decrement connector name...\n");
+                        decrement_name = true;
+                        XRRFreeOutputInfo(oinfo);
+                        break;
+                    }
+                    XRRFreeOutputInfo(oinfo);
+                }
+            }
+            // Compare the name of the xrandr output against what we would
+            // expect based on the drm connection type. The xrandr names
+            // are driver-specific, so we need to special-case some
+            // drivers.  Most hardware these days uses the 'modesetting'
+            // driver, but the QXL device uses its own driver which has
+            // different naming conventions
+            char expected_name[100];
+            if (vendor_id == PCI_VENDOR_ID_REDHAT && device_id == PCI_DEVICE_ID_QXL) {
+                drm_conn_name_qxl(conn, expected_name, sizeof(expected_name), decrement_name);
+            } else {
+                drm_conn_name_modesetting(conn, expected_name, sizeof(expected_name));
+            }
+
+            // Loop through xrandr outputs and check whether the xrandr
+            // output name matches the drm connector name
+            for (int i = 0; i < xres->noutput; ++i) {
+                int oid = xres->outputs[i];
+                XRROutputInfo *oinfo = XRRGetOutputInfo(xdisplay, xres, oid);
+
+                syslog(LOG_DEBUG, "Checking whether xrandr output %i (%s) matches expected name %s\n", i, oinfo->name, expected_name);
+                if (strcmp(oinfo->name, expected_name) == 0) {
+                    *output_id = oid;
+                    found_output = true;
+                    printf("    Found matching X Output:	name=%s id=%i (%s)\n",
+                           oinfo->name,
+                           (int)oid,
+                           connections[oinfo->connection + 1]);
+                    XRRFreeOutputInfo(oinfo);
+                    break;
+                }
+                XRRFreeOutputInfo(oinfo);
+            }
+            drmModeFreeConnector(conn);
+            conn = NULL;
+        } else {
+            syslog(LOG_WARNING, "Unable to get DRM resources for card");
+            // This is probably a proprietary driver (e.g. Nvidia) that does
+            // not provide outputs via drm, so the only thing we can do is just
+            // assume that it is the only device assigned to X, and use the
+            // xrandr output order to determine the proper display.
+            if (device_info->device_display_id >= xres->noutput) {
+                syslog(LOG_WARNING, "The device display id %i does not exist", device_info->device_display_id);
+                goto cleanup;
+            }
+            *output_id = xres->outputs[device_info->device_display_id];
+            found_output = true;
+        }
+        close(drm_fd);
+
+        if (!found_output) {
+            syslog(LOG_WARNING, "Couldn't find an XRandr output for the specified device");
+            goto cleanup;
+        }
+
+        // we found the right card, abort the loop
+        found_device = true;
+        break;
+    }
+
+cleanup:
+    if (xres) {
+        XRRFreeScreenResources(xres);
+    }
+    if (xdisplay) {
+        XCloseDisplay(xdisplay);
+    }
+    if (user_pci_addr) {
+        pci_address_free(user_pci_addr);
+    }
+    return found_device;
+}
diff --git a/src/vdagent/device-info.h b/src/vdagent/device-info.h
new file mode 100644
index 0000000..ec3ca32
--- /dev/null
+++ b/src/vdagent/device-info.h
@@ -0,0 +1,26 @@
+/*  device-info.c utility function for looking up the xrandr output id for a
+ *  given device address and display id
+ *
+ * Copyright 2018 Red Hat, Inc.
+ *
+ * Red Hat Authors:
+ * Jonathon Jongsma <jjongsma at redhat.com>
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#include <spice/vd_agent.h>
+#include <X11/extensions/Xrandr.h>
+#include <stdbool.h>
+
+bool lookup_xrandr_output_for_device_info(VDAgentGraphicsDeviceInfo *device_info, RROutput *output_id);
diff --git a/tests/test-device-info.c b/tests/test-device-info.c
new file mode 100644
index 0000000..5964313
--- /dev/null
+++ b/tests/test-device-info.c
@@ -0,0 +1,262 @@
+/*  test-device-info.c tests to see whether the functionality for converting a
+ *  device address to a xrandr output are working properly
+ *
+ * Copyright 2018 Red Hat, Inc.
+ *
+ * Red Hat Authors:
+ * Jonathon Jongsma <jjongsma at redhat.com>
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// just include the source file for testing
+#include "vdagent/device-info.c"
+
+#define assert_device(dev, domain_, bus_, slot_, function_) \
+{ \
+    PciDevice* dev_ = (dev); \
+    assert(dev_ != NULL); \
+    assert(dev_->domain == domain_); \
+    assert(dev_->bus == bus_); \
+    assert(dev_->slot == slot_); \
+    assert(dev_->function == function_); \
+}
+
+static void test_compare_addresses()
+{
+    {
+        PciDevice da1 = {1, 0, 3, 0};
+        PciDevice da2 = {1, 1, 1, 0};
+        PciDevice da3 = {1, 2, 3, 0};
+        PciAddress a1 = {1, NULL};
+        a1.domain = 0;
+        a1.devices = g_list_append(a1.devices, &da1);
+        a1.devices = g_list_append(a1.devices, &da2);
+        a1.devices = g_list_append(a1.devices, &da3);
+
+        PciDevice db1 = {1, 0, 3, 0};
+        PciDevice db2 = {1, 1, 1, 0};
+        PciDevice db3 = {1, 2, 3, 0};
+        PciAddress a2 = {1, NULL};
+        a2.domain = 0;
+        a2.devices = g_list_append(a2.devices, &db1);
+        a2.devices = g_list_append(a2.devices, &db2);
+        a2.devices = g_list_append(a2.devices, &db3);
+
+        assert(compare_addresses(&a1, &a2));
+    }
+    {
+        PciDevice da1 = {1, 0, 3, 0};
+        PciDevice da2 = {1, 1, 1, 0};
+        PciDevice da3 = {1, 2, 3, 0};
+        PciAddress a1 = {1, NULL};
+        a1.domain = 0;
+        a1.devices = g_list_append(a1.devices, &da1);
+        a1.devices = g_list_append(a1.devices, &da2);
+        a1.devices = g_list_append(a1.devices, &da3);
+
+        // a 'spice' format PCI address will not provide domain or bus for each
+        // device, only slot and function. So first two numbers for each device
+        // will always be set to 0
+        PciDevice db1 = {0, 0, 3, 0};
+        PciDevice db2 = {0, 0, 1, 0};
+        PciDevice db3 = {0, 0, 3, 0};
+        PciAddress a2 = {1, NULL};
+        a2.domain = 0;
+        a2.devices = g_list_append(a2.devices, &db1);
+        a2.devices = g_list_append(a2.devices, &db2);
+        a2.devices = g_list_append(a2.devices, &db3);
+
+        assert(compare_addresses(&a1, &a2));
+    }
+    // different number of devices
+    {
+        PciDevice da1 = {0, 0, 3, 0};
+        PciDevice da2 = {0, 1, 1, 0};
+        PciDevice da3 = {0, 2, 3, 0};
+        PciAddress a1 = {0, NULL};
+        a1.domain = 0;
+        a1.devices = g_list_append(a1.devices, &da1);
+        a1.devices = g_list_append(a1.devices, &da2);
+        a1.devices = g_list_append(a1.devices, &da3);
+
+        PciDevice db1 = {0, 0, 3, 0};
+        PciDevice db2 = {0, 1, 1, 0};
+        PciAddress a2 = {0, NULL};
+        a2.domain = 0;
+        a2.devices = g_list_append(a2.devices, &db1);
+        a2.devices = g_list_append(a2.devices, &db2);
+
+        assert(!compare_addresses(&a1, &a2));
+    }
+    // mismatched function
+    {
+        PciDevice da1 = {0, 0, 2, 0};
+        PciAddress a1 = {0, NULL};
+        a1.domain = 0;
+        a1.devices = g_list_append(a1.devices, &da1);
+
+        PciDevice db1 = {0, 0, 2, 1};
+        PciAddress a2 = {0, NULL};
+        a2.domain = 0;
+        a2.devices = g_list_append(a2.devices, &db1);
+
+        assert(!compare_addresses(&a1, &a2));
+    }
+    // mismatched slot
+    {
+        PciDevice da1 = {0, 0, 2, 0};
+        PciAddress a1 = {0, NULL};
+        a1.domain = 0;
+        a1.devices = g_list_append(a1.devices, &da1);
+
+        PciDevice db1 = {0, 0, 1, 0};
+        PciAddress a2 = {0, NULL};
+        a2.domain = 0;
+        a2.devices = g_list_append(a2.devices, &db1);
+
+        assert(!compare_addresses(&a1, &a2));
+    }
+    // mismatched domain
+    {
+        PciDevice da1 = {0, 0, 2, 0};
+        PciAddress a1 = {0, NULL};
+        a1.domain = 1;
+        a1.devices = g_list_append(a1.devices, &da1);
+
+        PciDevice db1 = {0, 0, 2, 0};
+        PciAddress a2 = {0, NULL};
+        a2.domain = 0;
+        a2.devices = g_list_append(a2.devices, &db1);
+
+        assert(!compare_addresses(&a1, &a2));
+    }
+}
+
+static void test_spice_parsing()
+{
+    PciAddress *addr = parse_pci_address_from_spice("pci/0000/02.0");
+    assert(addr != NULL);
+    assert(addr->domain == 0);
+    assert(g_list_length(addr->devices) == 1);
+    assert_device(addr->devices->data, 0, 0, 2, 0);
+    pci_address_free(addr);
+
+    addr = parse_pci_address_from_spice("pci/ffff/ff.f");
+    assert(addr != NULL);
+    assert(addr->domain == 65535);
+    assert(g_list_length(addr->devices) == 1);
+    assert_device(addr->devices->data, 0, 0, 255, 15);
+    pci_address_free(addr);
+
+    addr = parse_pci_address_from_spice("pci/0000/02.1/03.0");
+    assert(addr != NULL);
+    assert(addr->domain == 0);
+    assert(g_list_length(addr->devices) == 2);
+    assert_device(addr->devices->data, 0, 0, 2, 1);
+    assert_device(addr->devices->next->data, 0, 0, 3, 0);
+    pci_address_free(addr);
+
+    addr = parse_pci_address_from_spice("pci/000a/01.0/02.1/03.0");
+    assert(addr != NULL);
+    assert(addr->domain == 10);
+    assert(g_list_length(addr->devices) == 3);
+    assert_device(addr->devices->data, 0, 0, 1, 0);
+    assert_device(addr->devices->next->data, 0, 0, 2, 1);
+    assert_device(addr->devices->next->next->data, 0, 0, 3, 0);
+    pci_address_free(addr);
+
+    addr = parse_pci_address_from_spice("pcx/0000/02.1/03.0");
+    assert(addr == NULL);
+
+    addr = parse_pci_address_from_spice("0000/02.0");
+    assert(addr == NULL);
+
+    addr = parse_pci_address_from_spice("0000/02.1/03.0");
+    assert(addr == NULL);
+}
+
+static void test_sysfs_parsing()
+{
+    PciAddress *addr = parse_pci_address_from_sysfs_path("../../devices/pci0000:00/0000:00:02.0/drm/card0");
+    assert(addr != NULL);
+    assert(addr->domain == 0);
+    assert(g_list_length(addr->devices) == 1);
+    assert_device(addr->devices->data, 0, 0, 2, 0);
+    pci_address_free(addr);
+
+    addr = parse_pci_address_from_sysfs_path("../../devices/pciffff:ff/ffff:ff:ff.f/drm/card0");
+    assert(addr != NULL);
+    assert(addr->domain == 65535);
+    assert(g_list_length(addr->devices) == 1);
+    assert_device(addr->devices->data, 65535, 255, 255, 15);
+    pci_address_free(addr);
+
+    addr = parse_pci_address_from_sysfs_path("../../devices/pci0000:00/0000:00:03.0/0000:01:01.0/0000:02:03.0/virtio2/drm/card0");
+    assert(addr != NULL);
+    assert(addr->domain == 0);
+    assert(g_list_length(addr->devices) == 3);
+    assert_device(addr->devices->data, 0, 0, 3, 0);
+    assert_device(addr->devices->next->data, 0, 1, 1, 0);
+    assert_device(addr->devices->next->next->data, 0, 2, 3, 0);
+    pci_address_free(addr);
+}
+
+// verify that we parse the BDF notation correctly
+static bool test_bdf(const char* string, int domain, uint8_t bus, uint8_t slot, uint8_t function)
+{
+    PciDevice pci_dev;
+    return (parse_pci_device(string, NULL, &pci_dev)
+            && (pci_dev.domain == domain)
+            && (pci_dev.bus == bus)
+            && (pci_dev.slot == slot)
+            && (pci_dev.function == function));
+}
+
+static void test_bdf_parsing()
+{
+    // valid input
+    assert(test_bdf("0000:00:02.1", 0, 0, 2, 1));
+    assert(test_bdf("00:00:02.1", 0, 0, 2, 1));
+    assert(test_bdf("0000:00:03.0", 0, 0, 3, 0));
+    assert(test_bdf("0000:00:1d.1", 0, 0, 29, 1));
+    assert(test_bdf("0000:09:02.1", 0, 9, 2, 1));
+    assert(test_bdf("0000:1d:02.1", 0, 29, 2, 1));
+    assert(test_bdf("0000:00:02.d", 0, 0, 2, 13));
+    assert(test_bdf("000f:00:02.d", 15, 0, 2, 13));
+    assert(test_bdf("000f:00:02.d", 15, 0, 2, 13));
+    assert(test_bdf("000f:00:02.d", 15, 0, 2, 13));
+    assert(test_bdf("000f:00:02.d", 15, 0, 2, 13));
+    assert(test_bdf("ffff:ff:ff.f", 65535, 255, 255, 15));
+    assert(test_bdf("0:0:2.1", 0, 0, 2, 1));
+
+    // invalid input
+    assert(!test_bdf("0000:00:02:0", 0, 0, 2, 0));
+    assert(!test_bdf("-0001:00:02.1", -1, 0, 2, 1));
+    assert(!test_bdf("0000.00.02.0", 0, 0, 2, 0));
+    assert(!test_bdf("000f:00:02", 15, 0, 2, 0));
+    assert(!test_bdf("000f:00", 15, 0, 0, 0));
+    assert(!test_bdf("000f", 15, 0, 0, 0));
+    assert(!test_bdf("random string", 0, 0, 0, 0));
+    assert(!test_bdf("12345", 12345, 0, 0, 0));
+}
+
+int main(int argc, char **argv)
+{
+    test_bdf_parsing();
+    test_sysfs_parsing();
+    test_spice_parsing();
+    test_compare_addresses();
+}
+
-- 
2.17.2



More information about the Spice-devel mailing list