[spice-gtk,v3,1/2] cd-sharing: new source and header files

Submitted by Yuri Benditovich on Sept. 13, 2018, 10:08 a.m.

Details

Message ID 1536833306-29448-2-git-send-email-yuri.benditovich@daynix.com
State New
Headers show
Series "CD sharing feature" ( rev: 3 ) in Spice

Not browsing as part of any series.

Commit Message

Yuri Benditovich Sept. 13, 2018, 10:08 a.m.
This commit contains new source and header files,
related to cd-sharing feature.

Signed-off-by: Yuri Benditovich <yuri.benditovich@daynix.com>
Signed-off-by: Alexander Nezhinsky <alexander@daynix.com>
---
 src/cd-device-linux.c         |  137 ++
 src/cd-device-win.c           |  193 +++
 src/cd-device.h               |   40 +
 src/cd-scsi-dev-params.h      |   52 +
 src/cd-scsi.c                 | 2765 +++++++++++++++++++++++++++++++++++++++++
 src/cd-scsi.h                 |  122 ++
 src/cd-usb-bulk-msd.c         |  555 +++++++++
 src/cd-usb-bulk-msd.h         |  134 ++
 src/scsi-constants.h          |  324 +++++
 src/usb-backend-common.c      | 2015 ++++++++++++++++++++++++++++++
 src/usb-backend.h             |  112 ++
 src/usb-device-redir-widget.c | 2065 ++++++++++++++++++++++++++++++
 12 files changed, 8514 insertions(+)
 create mode 100644 src/cd-device-linux.c
 create mode 100644 src/cd-device-win.c
 create mode 100644 src/cd-device.h
 create mode 100644 src/cd-scsi-dev-params.h
 create mode 100644 src/cd-scsi.c
 create mode 100644 src/cd-scsi.h
 create mode 100644 src/cd-usb-bulk-msd.c
 create mode 100644 src/cd-usb-bulk-msd.h
 create mode 100644 src/scsi-constants.h
 create mode 100644 src/usb-backend-common.c
 create mode 100644 src/usb-backend.h
 create mode 100644 src/usb-device-redir-widget.c

Patch hide | download patch | download mbox

diff --git a/src/cd-device-linux.c b/src/cd-device-linux.c
new file mode 100644
index 0000000..c97e574
--- /dev/null
+++ b/src/cd-device-linux.c
@@ -0,0 +1,137 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+Copyright (C) 2018 Red Hat, Inc.
+
+Red Hat Authors:
+Yuri Benditovich<ybendito@redhat.com>
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "config.h"
+
+#include <glib-object.h>
+
+#ifndef G_OS_WIN32
+#ifdef USE_USBREDIR
+#include <inttypes.h>
+#include <gio/gio.h>
+#include "cd-device.h"
+#include "spice-client.h"
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <linux/fs.h>
+#include <linux/cdrom.h>
+
+int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
+{
+    int error = 0;
+    unit->device = 0;
+
+    if (!unit->filename && !filename) {
+        SPICE_DEBUG("%s: file name not provided", __FUNCTION__);
+        return -1; // TODO
+    }
+    if (unit->filename && filename) {
+        g_free(unit->filename);
+        unit->filename = NULL;
+    }
+    if (filename) {
+        unit->filename = g_strdup(filename);
+    }
+
+    int fd = open(unit->filename, O_RDONLY | O_NONBLOCK);
+    if (fd > 0) {
+        struct stat file_stat = { 0 };
+        if (fstat(fd, &file_stat) || file_stat.st_size == 0) {
+            file_stat.st_size = 0;
+            unit->device = 1;
+            if (!ioctl(fd, BLKGETSIZE64, &file_stat.st_size) &&
+                !ioctl(fd, BLKSSZGET, &unit->blockSize)) {
+            }
+        }
+        unit->size = file_stat.st_size;
+        close(fd);
+        if (unit->size) {
+            unit->file_object = g_file_new_for_path(unit->filename);
+            unit->stream = g_file_read(unit->file_object, NULL, NULL);
+        }
+        if (!unit->stream) {
+            SPICE_DEBUG("%s: can't open stream on %s", __FUNCTION__, unit->filename);
+            g_object_unref(unit->file_object);
+            unit->file_object = NULL;
+            error = -1; //TODO
+        }
+    }
+    else {
+        SPICE_DEBUG("%s: can't open file %s", __FUNCTION__, unit->filename);
+        error = -1; //TODO
+    }
+
+    return error;
+}
+
+int cd_device_load(SpiceCdLU *unit, gboolean load)
+{
+    int error;
+    if (!unit->device || !unit->filename) {
+        return -1; //TODO
+    }
+    int fd = open(unit->filename, O_RDONLY | O_NONBLOCK);
+    if (fd > 0) {
+        if (load) {
+            error = ioctl(fd, CDROMCLOSETRAY, 0);
+        } else {
+            error = ioctl(fd, CDROM_LOCKDOOR, 0);
+            error = ioctl(fd, CDROMEJECT, 0);
+        }
+        if (error) {
+            // note that ejecting might be available only for root
+            // loading might be available also for regular user
+            SPICE_DEBUG("%s: can't %sload %s, res %d, errno %d",
+                __FUNCTION__, load ? "" : "un", unit->filename, error, errno);
+        }
+        close(fd);
+    } else {
+        error = -1; //TODO
+    }
+    return error;
+}
+
+int cd_device_check(SpiceCdLU *unit)
+{
+    int error;
+    if (!unit->device || !unit->filename) {
+        return -1; //TODO
+    }
+    int fd = open(unit->filename, O_RDONLY | O_NONBLOCK);
+    if (fd > 0) {
+        error = ioctl(fd, CDROM_DRIVE_STATUS, 0);
+        error = (error == CDS_DISC_OK) ? 0 : -1;
+        if (!error) {
+            error = ioctl(fd, CDROM_DISC_STATUS, 0);
+            error = (error == CDS_DATA_1) ? 0 : -1;
+        }
+        close(fd);
+    }
+    else {
+        error = -1; //TODO
+    }
+    return error;
+}
+
+#endif
+#endif
diff --git a/src/cd-device-win.c b/src/cd-device-win.c
new file mode 100644
index 0000000..de7f3f1
--- /dev/null
+++ b/src/cd-device-win.c
@@ -0,0 +1,193 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+Copyright (C) 2018 Red Hat, Inc.
+
+Red Hat Authors:
+Yuri Benditovich<ybendito@redhat.com>
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "config.h"
+#include <glib-object.h>
+
+#ifdef G_OS_WIN32
+#ifdef USE_USBREDIR
+
+#include <inttypes.h>
+#include <gio/gio.h>
+#include <windows.h>
+#include <ntddcdrm.h>
+#include <ntddmmc.h>
+#include "cd-device.h"
+#include "spice-client.h"
+
+static gboolean is_device_name(const char *filename)
+{
+    gboolean b = strlen(filename) == 2 && filename[1] == ':';
+    return b;
+}
+
+static HANDLE open_file(const char *filename)
+{
+    HANDLE h = CreateFileA(
+        filename,
+        GENERIC_READ,
+        FILE_SHARE_READ | FILE_SHARE_WRITE,
+        NULL, OPEN_EXISTING,
+        0,
+        NULL);
+    if (h == INVALID_HANDLE_VALUE) {
+        h = NULL;
+    }
+    return h;
+}
+
+static uint32_t ioctl_out(HANDLE h, uint32_t code, void *out_buffer, uint32_t out_size)
+{
+    uint32_t error;
+    DWORD ret;
+    BOOL b = DeviceIoControl(h,
+        code,
+        NULL,
+        0,
+        out_buffer,
+        out_size,
+        &ret,
+        NULL);
+        error = b ? 0 : GetLastError();
+    return error;
+}
+
+static uint32_t ioctl_none(HANDLE h, uint32_t code)
+{
+    return ioctl_out(h, code, NULL, 0);
+}
+
+static gboolean check_device(HANDLE h)
+{
+    GET_CONFIGURATION_IOCTL_INPUT cfgIn = { FeatureCdRead, SCSI_GET_CONFIGURATION_REQUEST_TYPE_ALL };
+    DWORD ret;
+    GET_CONFIGURATION_HEADER cfgOut;
+    return DeviceIoControl(h, IOCTL_CDROM_GET_CONFIGURATION,
+        &cfgIn, sizeof(cfgIn), &cfgOut, sizeof(cfgOut),
+        &ret, NULL);
+}
+
+int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
+{
+    int error = 0;
+    HANDLE h;
+    unit->device = 0;
+    if (!unit->filename && !filename) {
+        SPICE_DEBUG("%s: unnamed file", __FUNCTION__);
+        return -1; // TODO
+    }
+    if (unit->filename && filename) {
+        g_free(unit->filename);
+        unit->filename = NULL;
+    }
+    if (!filename) {
+        // reopening the stream on existing file name
+    } else if (is_device_name(filename)) {
+        unit->filename = g_strdup_printf("\\\\.\\%s", filename);
+    } else {
+        unit->filename = g_strdup(filename);
+    }
+    h = open_file(unit->filename);
+    if (h) {
+        LARGE_INTEGER size = { 0 };
+        if (!GetFileSizeEx(h, &size)) {
+            uint64_t buffer[256];
+            unit->device = check_device(h);
+            SPICE_DEBUG("%s: CD device %srecognized on %s",
+                __FUNCTION__, unit->device ? "" : "NOT ", unit->filename);
+            uint32_t res = ioctl_out(h, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
+                buffer, sizeof(buffer));
+            if (!res)
+            {
+                DISK_GEOMETRY_EX *pg = (DISK_GEOMETRY_EX *)buffer;
+                unit->blockSize = pg->Geometry.BytesPerSector;
+                size = pg->DiskSize;
+            } else {
+                SPICE_DEBUG("%s: can't obtain size of %s (error %u)",
+                    __FUNCTION__, unit->filename, res);
+            }
+        }
+        unit->size = size.QuadPart;
+        CloseHandle(h);
+        if (unit->size) {
+            unit->file_object = g_file_new_for_path(unit->filename);
+            unit->stream = g_file_read(unit->file_object, NULL, NULL);
+        }
+        if (!unit->stream) {
+            SPICE_DEBUG("%s: can't open stream on %s", __FUNCTION__, unit->filename);
+            g_object_unref(unit->file_object);
+            unit->file_object = NULL;
+            error = -1; //TODO
+        }
+    } else {
+        SPICE_DEBUG("%s: can't open file %s", __FUNCTION__, unit->filename);
+        error = -1; //TODO
+    }
+    return error;
+}
+
+int cd_device_load(SpiceCdLU *unit, gboolean load)
+{
+    int error = 0;
+    HANDLE h;
+    if (!unit->device || !unit->filename) {
+        return -1; //TODO
+    }
+    h = open_file(unit->filename);
+    if (h) {
+        uint32_t res = ioctl_none(h, load ? IOCTL_STORAGE_LOAD_MEDIA : IOCTL_STORAGE_EJECT_MEDIA);
+        if (res) {
+            SPICE_DEBUG("%s: can't %sload %s, win error %u",
+                __FUNCTION__, load ? "" : "un", unit->filename, res);
+            error = -1; //TODO
+        } else {
+            SPICE_DEBUG("%s: device %s [%s]",
+                __FUNCTION__, load ? "loaded" : "ejected", unit->filename);
+        }
+        CloseHandle(h);
+    }
+    return error;
+}
+
+int cd_device_check(SpiceCdLU *unit)
+{
+    int error = 0;
+    CDROM_DISK_DATA data;
+    HANDLE h;
+    if (!unit->device || !unit->filename) {
+        return -1; //TODO
+    }
+    h = open_file(unit->filename);
+    if (h) {
+        uint32_t res = ioctl_none(h, IOCTL_STORAGE_CHECK_VERIFY);
+        if (!res) {
+            res = ioctl_out(h, IOCTL_CDROM_DISK_TYPE, &data, sizeof(data));
+        }
+        if (res != 0 || data.DiskData != CDROM_DISK_DATA_TRACK) {
+            error = -1; //TODO
+        }
+        CloseHandle(h);
+    }
+    return error;
+}
+
+#endif
+#endif
diff --git a/src/cd-device.h b/src/cd-device.h
new file mode 100644
index 0000000..050c7a1
--- /dev/null
+++ b/src/cd-device.h
@@ -0,0 +1,40 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+Copyright (C) 2018 Red Hat, Inc.
+
+Red Hat Authors:
+Yuri Benditovich<ybendito@redhat.com>
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef __CD_DEVICE_H__
+#define __CD_DEVICE_H__
+
+typedef struct _SpiceCdLU
+{
+    char *filename;
+    GFile *file_object;
+    GFileInputStream *stream;
+    uint64_t size;
+    uint32_t blockSize;
+    uint32_t loaded : 1;
+    uint32_t device : 1;
+} SpiceCdLU;
+
+int cd_device_open_stream(SpiceCdLU *unit, const char *filename);
+int cd_device_load(SpiceCdLU *unit, gboolean load);
+int cd_device_check(SpiceCdLU *unit);
+
+#endif
diff --git a/src/cd-scsi-dev-params.h b/src/cd-scsi-dev-params.h
new file mode 100644
index 0000000..cddce0a
--- /dev/null
+++ b/src/cd-scsi-dev-params.h
@@ -0,0 +1,52 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   CD SCSI device parameters
+
+   Copyright (C) 2018 Red Hat, Inc.
+
+   Red Hat Authors:
+   Alexander Nezhinsky<anezhins@redhat.com>
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   This library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef _CD_SCSI_DEV_PARAMS_H_
+#define _CD_SCSI_DEV_PARAMS_H_
+
+#include <gio/gio.h>
+
+typedef struct _CdScsiDeviceParameters
+{
+    const char *vendor;
+    const char *product;
+    const char *version;
+    const char *serial;
+} CdScsiDeviceParameters;
+
+typedef struct _CdScsiDeviceInfo
+{
+    CdScsiDeviceParameters parameters;
+    uint32_t started    : 1;
+    uint32_t locked     : 1;
+    uint32_t loaded     : 1;
+} CdScsiDeviceInfo;
+
+typedef struct _CdScsiMediaParameters
+{
+    GFileInputStream *stream;
+    uint64_t size;
+    uint32_t block_size;
+} CdScsiMediaParameters;
+
+#endif /* _CD_SCSI_DEV_PARAMS_H_ */
diff --git a/src/cd-scsi.c b/src/cd-scsi.c
new file mode 100644
index 0000000..86baf97
--- /dev/null
+++ b/src/cd-scsi.c
@@ -0,0 +1,2765 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   CD device emulation - SCSI engine
+
+   Copyright (C) 2018 Red Hat, Inc.
+
+   Red Hat Authors:
+   Alexander Nezhinsky<anezhins@redhat.com>
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   This library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "config.h"
+#include "spice/types.h"
+#include "spice-common.h"
+#include "spice-util.h"
+#include "cd-scsi.h"
+
+#ifdef USE_USBREDIR
+
+#define SPICE_ERROR(fmt, ...) \
+    do { SPICE_DEBUG("dev-scsi error: " fmt , ## __VA_ARGS__); } while (0)
+
+#define MAX_LUNS   32
+
+struct _CdScsiTarget; /* forward declaration */
+typedef struct _CdScsiTarget CdScsiTarget;
+
+typedef enum _CdScsiPowerCondition {
+    CD_SCSI_POWER_STOPPED,
+    CD_SCSI_POWER_ACTIVE,
+    CD_SCSI_POWER_IDLE,
+    CD_SCSI_POWER_STANDBY
+} CdScsiPowerCondition;
+
+typedef struct _ScsiShortSense
+{
+    uint8_t key;
+    uint8_t asc;
+    uint8_t ascq;
+    const char *descr;
+} ScsiShortSense;
+
+#define CD_MEDIA_EVENT_NO_CHANGE            0x0
+#define CD_MEDIA_EVENT_EJECT_REQ            0x1 /* user request (mechanical switch) to eject the media */
+#define CD_MEDIA_EVENT_NEW_MEDIA            0x2 /* new media received */
+#define CD_MEDIA_EVENT_MEDIA_REMOVAL        0x3 /* media removed */
+#define CD_MEDIA_EVENT_MEDIA_CHANGED        0x4 /* user request to load new media */
+#define CD_MEDIA_EVENT_BG_FORMAT_COMPLETE   0x5
+#define CD_MEDIA_EVENT_BG_FORMAT_RESTART    0x6
+
+#define CD_POWER_EVENT_NO_CHANGE            0x0
+#define CD_POWER_EVENT_CHANGE_SUCCESS       0x1
+#define CD_POWER_EVENT_CHANGE_FALED         0x2
+
+typedef struct _CdScsiLU
+{
+    CdScsiTarget *tgt;
+    uint32_t lun;
+
+    gboolean realized;
+    gboolean removable;
+    gboolean loaded;
+    gboolean prevent_media_removal;
+    gboolean cd_rom;
+
+    CdScsiPowerCondition power_cond;
+    uint32_t power_event;
+    uint32_t media_event;
+
+    uint32_t claim_version;
+
+    uint64_t size;
+    uint32_t block_size;
+    uint32_t num_blocks;
+
+    char *vendor;
+    char *product;
+    char *version;
+    char *serial;
+
+    GFileInputStream *stream;
+
+    ScsiShortSense short_sense; /* currently held sense of the scsi device */
+    uint8_t fixed_sense[FIXED_SENSE_LEN];
+} CdScsiLU;
+
+typedef enum _CdScsiTargetState
+{
+    CD_SCSI_TGT_STATE_RUNNING,
+    CD_SCSI_TGT_STATE_RESET,
+} CdScsiTargetState;
+
+struct _CdScsiTarget
+{
+    void *user_data;
+
+    CdScsiTargetState state;
+    CdScsiRequest *cur_req;
+    GCancellable *cancellable;
+
+    uint32_t num_luns;
+    uint32_t max_luns;
+    CdScsiLU units[MAX_LUNS];
+};
+
+static gboolean cmd_names_initialized = FALSE;
+static const char* scsi_cmd_name[256];
+
+/* Predefined sense codes */
+
+const ScsiShortSense sense_code_NO_SENSE = {
+    .key = NO_SENSE , .asc = 0x00 , .ascq = 0x00,
+    .descr = ""
+};
+
+const ScsiShortSense sense_code_NOT_READY_CAUSE_NOT_REPORTABLE = {
+    .key = NOT_READY, .asc = 0x04, .ascq = 0x00,
+    .descr = "CAUSE NOT REPORTABLE"
+};
+
+const ScsiShortSense sense_code_BECOMING_READY = {
+    .key = NOT_READY, .asc = 0x04, .ascq = 0x01,
+    .descr = "IN PROCESS OF BECOMING READY"
+};
+
+const ScsiShortSense sense_code_INIT_CMD_REQUIRED = {
+    .key = NOT_READY, .asc = 0x04, .ascq = 0x02,
+    .descr = "INITIALIZING COMMAND REQUIRED"
+};
+
+const ScsiShortSense sense_code_INTERVENTION_REQUIRED = {
+    .key = NOT_READY, .asc = 0x04, .ascq = 0x03,
+    .descr = "MANUAL INTERVENTION REQUIRED"
+};
+
+const ScsiShortSense sense_code_NOT_READY_NO_MEDIUM = {
+    .key = NOT_READY, .asc = 0x3a, .ascq = 0x00,
+    .descr = "MEDIUM NOT PRESENT"
+};
+
+const ScsiShortSense sense_code_NOT_READY_NO_MEDIUM_TRAY_CLOSED = {
+    .key = NOT_READY, .asc = 0x3a, .ascq = 0x01,
+    .descr = "MEDIUM NOT PRESENT - TRAY CLOSED"
+};
+
+const ScsiShortSense sense_code_NOT_READY_NO_MEDIUM_TRAY_OPEN = {
+    .key = NOT_READY, .asc = 0x3a, .ascq = 0x02,
+    .descr = "MEDIUM NOT PRESENT - TRAY OPEN"
+};
+
+const ScsiShortSense sense_code_TARGET_FAILURE = {
+    .key = HARDWARE_ERROR, .asc = 0x44, .ascq = 0x00,
+    .descr = "INTERNAL TARGET FAILURE"
+};
+
+const ScsiShortSense sense_code_INVALID_OPCODE = {
+    .key = ILLEGAL_REQUEST, .asc = 0x20, .ascq = 0x00,
+    .descr = "INVALID COMMAND OPERATION CODE"
+};
+
+const ScsiShortSense sense_code_LBA_OUT_OF_RANGE = {
+    .key = ILLEGAL_REQUEST, .asc = 0x21, .ascq = 0x00,
+    .descr = "LOGICAL BLOCK ADDRESS OUT OF RANGE"
+};
+
+const ScsiShortSense sense_code_INVALID_CDB_FIELD = {
+    .key = ILLEGAL_REQUEST, .asc = 0x24, .ascq = 0x00,
+    .descr = "INVALID FIELD IN CDB"
+};
+
+const ScsiShortSense sense_code_INVALID_PARAM_FIELD = {
+    .key = ILLEGAL_REQUEST, .asc = 0x26, .ascq = 0x00,
+    .descr = "INVALID FIELD IN PARAMETER LIST"
+};
+
+const ScsiShortSense sense_code_INVALID_PARAM_LEN = {
+    .key = ILLEGAL_REQUEST, .asc = 0x1a, .ascq = 0x00,
+    .descr = "PARAMETER LIST LENGTH ERROR"
+};
+
+const ScsiShortSense sense_code_LUN_NOT_SUPPORTED = {
+    .key = ILLEGAL_REQUEST, .asc = 0x25, .ascq = 0x00,
+    .descr = "LOGICAL UNIT NOT SUPPORTED"
+};
+
+const ScsiShortSense sense_code_SAVING_PARAMS_NOT_SUPPORTED = {
+    .key = ILLEGAL_REQUEST, .asc = 0x39, .ascq = 0x00,
+    .descr = "SAVING PARAMETERS NOT SUPPORTED"
+};
+
+const ScsiShortSense sense_code_INCOMPATIBLE_MEDIUM = {
+    .key = ILLEGAL_REQUEST, .asc = 0x30, .ascq = 0x00,
+    .descr = "INCOMPATIBLE MEDIUM INSTALLED"
+};
+
+const ScsiShortSense sense_code_MEDIUM_REMOVAL_PREVENTED = {
+    .key = ILLEGAL_REQUEST, .asc = 0x53, .ascq = 0x02,
+    .descr = "MEDIUM REMOVAL PREVENTED"
+};
+
+const ScsiShortSense sense_code_PARAMETERS_CHANGED = {
+    .key = UNIT_ATTENTION, .asc = 0x2a, .ascq = 0x00,
+    .descr = "PARAMETERS CHANGED"
+};
+
+const ScsiShortSense sense_code_POWER_ON_RESET = {
+    .key = UNIT_ATTENTION, .asc = 0x29, .ascq = 0x00,
+    .descr = "POWER ON, RESET, OR BUS DEVICE RESET"
+};
+
+const ScsiShortSense sense_code_SCSI_BUS_RESET = {
+    .key = UNIT_ATTENTION, .asc = 0x29, .ascq = 0x02,
+    .descr = "SCSI BUS RESET"
+};
+
+const ScsiShortSense sense_code_UA_NO_MEDIUM = {
+    .key = UNIT_ATTENTION, .asc = 0x3a, .ascq = 0x00,
+    .descr = "MEDIUM NOT PRESENT"
+};
+
+const ScsiShortSense sense_code_MEDIUM_CHANGED = {
+    .key = UNIT_ATTENTION, .asc = 0x28, .ascq = 0x00,
+    .descr = "MEDIUM CHANGED"
+};
+
+const ScsiShortSense sense_code_REPORTED_LUNS_CHANGED = {
+    .key = UNIT_ATTENTION, .asc = 0x3f, .ascq = 0x0e,
+    .descr = "REPORTED LUNS CHANGED"
+};
+
+const ScsiShortSense sense_code_DEVICE_INTERNAL_RESET = {
+    .key = UNIT_ATTENTION, .asc = 0x29, .ascq = 0x04,
+    .descr = "DEVICE INTERNAL RESET"
+};
+
+const ScsiShortSense sense_code_UNIT_ATTENTION_MEDIUM_REMOVAL_REQUEST = {
+    .key = UNIT_ATTENTION, .asc = 0x5a, .ascq = 0x01,
+    .descr = "OPERATOR MEDIUM REMOVAL REQUEST"
+};
+
+static inline gboolean cd_scsi_opcode_ua_supress(uint32_t opcode)
+{
+    switch (opcode) {
+    case INQUIRY:
+    case REPORT_LUNS:
+    case GET_CONFIGURATION:
+    case GET_EVENT_STATUS_NOTIFICATION:
+    case REQUEST_SENSE:
+        return TRUE;
+    default:
+        return FALSE;
+    }
+}
+
+static inline const char *CdScsiReqState_str(CdScsiReqState state)
+{
+    switch(state) {
+    case SCSI_REQ_IDLE:
+        return "IDLE";
+    case SCSI_REQ_RUNNING:
+        return "RUNNING";
+    case SCSI_REQ_COMPLETE:
+        return "COMPLETE";
+    case SCSI_REQ_CANCELED:
+        return "CANCELED";
+    default:
+        return "ILLEGAL";
+    }
+}
+
+static const char *cd_scsi_sense_key_descr(uint8_t sense_key)
+{
+    switch(sense_key) {
+    case NO_SENSE:
+        return "NO SENSE";
+    case RECOVERED_ERROR:
+        return "RECOVERED ERROR";
+    case NOT_READY:
+        return "LUN NOT READY";
+    case MEDIUM_ERROR:
+        return "MEDIUM ERROR";
+    case HARDWARE_ERROR:
+        return "HARDWARE ERROR";
+    case ILLEGAL_REQUEST:
+        return "ILLEGAL REQUEST";
+    case UNIT_ATTENTION:
+        return "UNIT ATTENTION";
+    case BLANK_CHECK:
+        return "BLANK CHECK";
+    case ABORTED_COMMAND:
+        return "ABORTED COMMAND";
+    default:
+        return "???";
+    }
+}
+static uint32_t cd_scsi_build_fixed_sense(uint8_t *buf, const ScsiShortSense *short_sense)
+{
+    memset(buf, 0, FIXED_SENSE_LEN);
+
+    buf[0] = FIXED_SENSE_CURRENT;
+    buf[2] = short_sense->key;
+    buf[7] = 10;
+    buf[12] = short_sense->asc;
+    buf[13] = short_sense->ascq;
+
+    return FIXED_SENSE_LEN;
+}
+
+static inline void cd_scsi_req_init(CdScsiRequest *req)
+{
+    req->req_state = SCSI_REQ_IDLE;
+    req->xfer_dir = SCSI_XFER_NONE;
+    req->priv_data = NULL;
+    req->in_len = 0;
+    req->status = GOOD;
+}
+
+static inline void cd_scsi_dev_sense_reset(CdScsiLU *dev)
+{
+    memset(&dev->short_sense, 0, sizeof(dev->short_sense));
+    cd_scsi_build_fixed_sense(dev->fixed_sense, &dev->short_sense);
+}
+
+static inline void cd_scsi_dev_sense_set(CdScsiLU *dev, const ScsiShortSense *short_sense)
+{
+    if (short_sense != NULL) {
+        /* copy short sense and generate full sense in fixed format */
+        dev->short_sense = *short_sense;
+        cd_scsi_build_fixed_sense(dev->fixed_sense, short_sense);
+    }
+}
+
+static inline void cd_scsi_dev_sense_set_power_on(CdScsiLU *dev)
+{
+    cd_scsi_dev_sense_set(dev, &sense_code_POWER_ON_RESET);
+}
+
+static void cd_scsi_cmd_complete_check_cond(CdScsiLU *dev, CdScsiRequest *req,
+                                            const ScsiShortSense *short_sense)
+{
+    req->req_state = SCSI_REQ_COMPLETE;
+    req->status = CHECK_CONDITION;
+    req->in_len = 0;
+
+    cd_scsi_dev_sense_set(dev, short_sense);
+
+    SPICE_DEBUG("CHECK_COND, request lun:%" G_GUINT32_FORMAT
+                " op: 0x%02x, pending sense: 0x%02x %02x %02x - %s, %s",
+                dev->lun, (uint32_t)req->cdb[0],
+                (uint32_t)dev->short_sense.key,
+                (uint32_t)dev->short_sense.asc,
+                (uint32_t)dev->short_sense.ascq,
+                cd_scsi_sense_key_descr(dev->short_sense.key),
+                dev->short_sense.descr);
+}
+
+static void cd_scsi_cmd_complete_good(CdScsiLU *dev, CdScsiRequest *req)
+{
+    req->req_state = SCSI_REQ_COMPLETE;
+    req->status = GOOD;
+}
+
+/* SCSI Target */
+
+static void cd_scsi_cmd_names_init(void)
+{
+    uint32_t opcode;
+
+    if (cmd_names_initialized) {
+        return;
+    }
+
+    for (opcode = 0; opcode < 256; opcode++) {
+        scsi_cmd_name[opcode] = "UNSUPPORTED";
+    }
+
+    scsi_cmd_name[REPORT_LUNS] = "REPORT LUNS";
+    scsi_cmd_name[TEST_UNIT_READY] = "TEST UNIT READY";
+    scsi_cmd_name[INQUIRY] = "INQUIRY";
+    scsi_cmd_name[REQUEST_SENSE] = "REQUEST SENSE";
+    scsi_cmd_name[READ_6] = "READ(6)";
+    scsi_cmd_name[READ_10] = "READ(10)";
+    scsi_cmd_name[READ_12] = "READ(12)";
+    scsi_cmd_name[READ_16] = "READ(16)";
+    scsi_cmd_name[READ_CAPACITY_10] = "READ CAPACITY(10)";
+    scsi_cmd_name[READ_TOC] = "READ TOC";
+    scsi_cmd_name[GET_EVENT_STATUS_NOTIFICATION] = "GET EVENT/STATUS NOTIFICATION";
+    scsi_cmd_name[READ_DISC_INFORMATION] = "READ DISC INFO";
+    scsi_cmd_name[READ_TRACK_INFORMATION] = "READ TRACK INFO";
+    scsi_cmd_name[MODE_SENSE_10] = "MODE SENSE(10)";
+    scsi_cmd_name[MODE_SELECT] = "MODE SELECT(6)";
+    scsi_cmd_name[MODE_SELECT_10] = "MODE SELECT(10)";
+    scsi_cmd_name[GET_CONFIGURATION] = "GET CONFIGURATION";
+    scsi_cmd_name[ALLOW_MEDIUM_REMOVAL] = "PREVENT ALLOW MEDIUM REMOVAL";
+    scsi_cmd_name[MMC_SEND_EVENT] = "SEND EVENT";
+    scsi_cmd_name[MMC_REPORT_KEY] = "REPORT KEY";
+    scsi_cmd_name[MMC_SEND_KEY] = "SEND_KEY";
+    scsi_cmd_name[START_STOP] = "START STOP UNIT";
+    scsi_cmd_name[MMC_GET_PERFORMANCE] = "GET PERFORMANCE";
+    scsi_cmd_name[MMC_MECHANISM_STATUS] = "MECHANISM STATUS";
+
+    cmd_names_initialized = TRUE;
+}
+
+void *cd_scsi_target_alloc(void *target_user_data, uint32_t max_luns)
+{
+    CdScsiTarget *st;
+
+    if (max_luns == 0 || max_luns > MAX_LUNS) {
+        SPICE_ERROR("Alloc, illegal max_luns:%" G_GUINT32_FORMAT, max_luns);
+        return NULL;
+    }
+
+    st = g_malloc0(sizeof(*st));
+
+    st->user_data = target_user_data;
+    st->state = CD_SCSI_TGT_STATE_RUNNING;
+    st->cur_req = NULL;
+    st->cancellable = g_cancellable_new();
+    st->max_luns = max_luns;
+
+    cd_scsi_cmd_names_init();
+
+    return (void *)st;
+}
+
+void cd_scsi_target_free(void *scsi_target)
+{
+    cd_scsi_target_reset(scsi_target);
+    g_free(scsi_target);
+}
+
+/* SCSI Device */
+
+static inline gboolean cd_scsi_target_lun_legal(CdScsiTarget *st, uint32_t lun)
+{
+    return (lun < st->max_luns) ? TRUE : FALSE;
+}
+
+static inline gboolean cd_scsi_target_lun_realized(CdScsiTarget *st, uint32_t lun)
+{
+    return (st->num_luns == 0 || !st->units[lun].realized) ? FALSE : TRUE;
+}
+
+int cd_scsi_dev_realize(void *scsi_target, uint32_t lun,
+                        const CdScsiDeviceParameters *dev_params)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    CdScsiLU *dev;
+
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("Realize, illegal lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    if (cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("Realize, already realized lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    dev = &st->units[lun];
+
+    memset(dev, 0, sizeof(*dev));
+    dev->tgt = st;
+    dev->lun = lun;
+
+    dev->realized = TRUE;
+    dev->removable = TRUE;
+    dev->loaded = FALSE;
+    dev->prevent_media_removal = FALSE;
+    dev->cd_rom = FALSE;
+
+    dev->power_cond = CD_SCSI_POWER_ACTIVE;
+    dev->power_event = CD_POWER_EVENT_NO_CHANGE;
+    dev->media_event = CD_MEDIA_EVENT_NO_CHANGE;
+
+    dev->claim_version = 3; /* 0 : none; 2,3,5 : SPC/MMC-x */
+
+    dev->vendor = g_strdup(dev_params->vendor);
+    dev->product = g_strdup(dev_params->product);
+    dev->version = g_strdup(dev_params->version);
+    dev->serial = g_strdup(dev_params->serial);
+
+    cd_scsi_dev_sense_set_power_on(dev);
+
+    st->num_luns ++;
+
+    SPICE_DEBUG("Realize lun:%" G_GUINT32_FORMAT " bs:%" G_GUINT32_FORMAT
+                " VR:[%s] PT:[%s] ver:[%s] SN[%s]",
+                lun, dev->block_size, dev->vendor,
+                dev->product, dev->version, dev->serial);
+    return 0;
+}
+
+static void cd_scsi_lu_media_reset(CdScsiLU *dev)
+{
+    /* media_event is not set here, as it depends on the context */
+    dev->stream = NULL;
+    dev->size = 0;
+    dev->block_size = 0;
+    dev->num_blocks = 0;
+}
+
+int cd_scsi_dev_lock(void *scsi_target, uint32_t lun, gboolean lock)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    CdScsiLU *dev;
+
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("Lock, illegal lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    if (!cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("Lock, unrealized lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    dev = &st->units[lun];
+    dev->prevent_media_removal = lock;
+    SPICE_DEBUG("lun:%" G_GUINT32_FORMAT "%slock", lun, lock ? "un" :"");
+    return 0;
+}
+
+static void cd_scsi_lu_load(CdScsiLU *dev,
+                            const CdScsiMediaParameters *media_params)
+{
+    if (media_params != NULL) {
+        dev->media_event = CD_MEDIA_EVENT_NEW_MEDIA;
+        dev->stream = media_params->stream;
+        dev->size = media_params->size;
+        dev->block_size = media_params->block_size;
+        dev->num_blocks = media_params->size / media_params->block_size;
+    } else {
+        dev->media_event = CD_MEDIA_EVENT_MEDIA_REMOVAL;
+        cd_scsi_lu_media_reset(dev);
+    }
+    dev->loaded = TRUE;
+}
+
+static void cd_scsi_lu_unload(CdScsiLU *dev)
+{
+    dev->media_event = CD_MEDIA_EVENT_MEDIA_REMOVAL;
+    cd_scsi_lu_media_reset(dev);
+    dev->loaded = FALSE;
+}
+
+int cd_scsi_dev_load(void *scsi_target, uint32_t lun,
+                     const CdScsiMediaParameters *media_params)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    CdScsiLU *dev;
+
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("Load, illegal lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    if (!cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("Load, unrealized lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    dev = &st->units[lun];
+
+    cd_scsi_lu_load(dev, media_params);
+    dev->power_cond = CD_SCSI_POWER_ACTIVE;
+    dev->power_event = CD_POWER_EVENT_CHANGE_SUCCESS;
+
+    cd_scsi_dev_sense_set(dev, &sense_code_MEDIUM_CHANGED);
+
+    SPICE_DEBUG("Load lun:%" G_GUINT32_FORMAT " size:%" G_GUINT64_FORMAT
+                " blk_sz:%" G_GUINT32_FORMAT " num_blocks:%" G_GUINT32_FORMAT,
+                lun, dev->size, dev->block_size, dev->num_blocks);
+    return 0;
+}
+
+int cd_scsi_dev_get_info(void *scsi_target, uint32_t lun, CdScsiDeviceInfo *lun_info)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    CdScsiLU *dev;
+
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("Load, illegal lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    if (!cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("Load, unrealized lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    dev = &st->units[lun];
+
+    lun_info->started = dev->power_cond == CD_SCSI_POWER_ACTIVE;
+    lun_info->locked = dev->prevent_media_removal;
+    lun_info->loaded = dev->loaded;
+
+    lun_info->parameters.vendor = dev->vendor;
+    lun_info->parameters.product = dev->product;
+    lun_info->parameters.version = dev->version;
+    lun_info->parameters.serial = dev->serial;
+
+    return 0;
+}
+
+int cd_scsi_dev_unload(void *scsi_target, uint32_t lun)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    CdScsiLU *dev;
+
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("Unload, illegal lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    if (!cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("Unload, unrealized lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    dev = &st->units[lun];
+    if (!dev->loaded) {
+        SPICE_ERROR("Unload, lun:%" G_GUINT32_FORMAT " not loaded yet", lun);
+        return 0;
+    }
+    if (dev->prevent_media_removal) {
+        SPICE_ERROR("Unload, lun:%" G_GUINT32_FORMAT " prevent_media_removal set", lun);
+        return -1;
+    }
+
+    cd_scsi_lu_unload(dev);
+    dev->power_cond = CD_SCSI_POWER_STOPPED;
+    dev->power_event = CD_POWER_EVENT_CHANGE_SUCCESS;
+
+    cd_scsi_dev_sense_set(dev, &sense_code_UA_NO_MEDIUM);
+
+    SPICE_DEBUG("Unload lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+int cd_scsi_dev_unrealize(void *scsi_target, uint32_t lun)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    CdScsiLU *dev;
+
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("Unrealize, illegal lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    if (!cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("Unrealize, absent lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    dev = &st->units[lun];
+
+    if (dev->vendor != NULL) {
+        free(dev->vendor);
+        dev->vendor = NULL;
+    }
+    if (dev->product != NULL) {
+        free(dev->product);
+        dev->product = NULL;
+    }
+    if (dev->version != NULL) {
+        free(dev->version);
+        dev->version = NULL;
+    }
+    if (dev->serial != NULL) {
+        free(dev->serial);
+        dev->serial = NULL;
+    }
+
+    dev->loaded = FALSE;
+    dev->realized = FALSE;
+    dev->power_cond = CD_SCSI_POWER_STOPPED;
+
+    st->num_luns --;
+
+    SPICE_DEBUG("Unrealize lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+int cd_scsi_dev_reset(void *scsi_target, uint32_t lun)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    CdScsiLU *dev;
+
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("Device reset, illegal lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    if (!cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("Device reset, absent lun:%" G_GUINT32_FORMAT, lun);
+        return -1;
+    }
+    dev = &st->units[lun];
+
+    dev->prevent_media_removal = FALSE;
+    dev->power_cond = CD_SCSI_POWER_ACTIVE;
+    dev->power_event = CD_POWER_EVENT_CHANGE_SUCCESS;
+    cd_scsi_dev_sense_set_power_on(dev);
+
+    SPICE_DEBUG("Device reset lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+static void cd_scsi_target_do_reset(CdScsiTarget *st)
+{
+    uint32_t lun;
+
+    for (lun = 0; lun < st->max_luns; lun++) {
+        if (st->units[lun].realized) {
+            cd_scsi_dev_reset(st, lun);
+        }
+    }
+
+    SPICE_DEBUG("Target reset complete");
+    st->state = CD_SCSI_TGT_STATE_RUNNING;
+    cd_scsi_target_reset_complete(st->user_data);
+}
+
+int cd_scsi_target_reset(void *scsi_target)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+
+    if (st->state == CD_SCSI_TGT_STATE_RESET) {
+        SPICE_DEBUG("Target already in reset");
+        return -1;
+    }
+
+    st->state = CD_SCSI_TGT_STATE_RESET;
+
+    if (st->cur_req != NULL) {
+        cd_scsi_dev_request_cancel(scsi_target, st->cur_req);
+        if (st->cur_req != NULL) {
+            SPICE_DEBUG("Target reset in progress...");
+            return 0;
+        }
+    }
+
+    cd_scsi_target_do_reset(st);
+    return 0;
+}
+
+CdScsiReqState cd_scsi_get_req_state(CdScsiRequest *req)
+{
+    return req->req_state;
+}
+
+static void strpadcpy(char *buf, int buf_size, const char *str, char pad)
+{
+    int len = strnlen(str, buf_size);
+    memcpy(buf, str, len);
+    memset(buf + len, pad, buf_size - len);
+}
+
+/* SCSI CDB */
+
+static int scsi_cdb_length(uint8_t *cdb)
+{
+    int cdb_len;
+
+    switch (cdb[0] >> 5) {
+    case 0:
+        cdb_len = 6;
+        break;
+    case 1:
+    case 2:
+        cdb_len = 10;
+        break;
+    case 4:
+        cdb_len = 16;
+        break;
+    case 5:
+        cdb_len = 12;
+        break;
+    default:
+        cdb_len = -1;
+    }
+    return cdb_len;
+}
+
+static uint64_t scsi_cdb_lba(uint8_t *cdb, int cdb_len)
+{
+    uint64_t lba;
+
+    switch (cdb_len) {
+    case 6:
+        lba = (((uint64_t)(cdb[1] & 0x1f)) << 16) |
+              (((uint64_t)cdb[2]) << 8) |
+               ((uint64_t)cdb[3]);
+        break;
+    case 10:
+    case 12:
+        lba = (((uint64_t)cdb[2]) << 24) |
+              (((uint64_t)cdb[3]) << 16) |
+              (((uint64_t)cdb[4]) << 8)  |
+               ((uint64_t)cdb[5]);
+        break;
+    case 16:
+        lba = (((uint64_t)cdb[2]) << 56) |
+              (((uint64_t)cdb[3]) << 48) |
+              (((uint64_t)cdb[4]) << 40) |
+              (((uint64_t)cdb[5]) << 32) |
+              (((uint64_t)cdb[6]) << 24) |
+              (((uint64_t)cdb[7]) << 16) |
+              (((uint64_t)cdb[8]) << 8)  |
+               ((uint64_t)cdb[9]);
+        break;
+    default:
+        lba = 0;
+    }
+    return lba;
+}
+
+static uint32_t scsi_cdb_xfer_length(uint8_t *cdb, int cdb_len)
+{
+    uint32_t len;
+
+    switch (cdb_len) {
+    case 6:
+        len = (uint32_t)cdb[4];
+        if (len == 0)
+            len = 256;
+        break;
+    case 10:
+        len = (((uint32_t)cdb[7]) << 8) |
+               ((uint32_t)cdb[8]);
+        break;
+    case 12:
+        len = (((uint32_t)cdb[6]) << 24) |
+              (((uint32_t)cdb[7]) << 16) |
+              (((uint32_t)cdb[8]) << 8)  |
+               ((uint32_t)cdb[9]);
+        break;
+    case 16:
+        len = (((uint32_t)cdb[10]) << 24) |
+              (((uint32_t)cdb[11]) << 16) |
+              (((uint32_t)cdb[12]) << 8)  |
+               ((uint32_t)cdb[13]);
+        break;
+    default:
+        len = 0;
+        break;
+    }
+    return len;
+}
+
+/* SCSI commands */
+
+static void cd_scsi_cmd_test_unit_ready(CdScsiLU *dev, CdScsiRequest *req)
+{
+    req->xfer_dir = SCSI_XFER_NONE;
+    req->in_len = 0;
+
+    if (dev->loaded) {
+        if (dev->power_cond != CD_SCSI_POWER_STOPPED) {
+            cd_scsi_cmd_complete_good(dev, req);
+        } else {
+            cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INIT_CMD_REQUIRED);
+        }
+    } else {
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_NOT_READY_NO_MEDIUM);
+    }
+}
+
+static void cd_scsi_cmd_request_sense(CdScsiLU *dev, CdScsiRequest *req)
+{
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    req->req_len = req->cdb[4];
+    req->in_len = (req->req_len < sizeof(dev->fixed_sense)) ?
+                   req->req_len : sizeof(dev->fixed_sense);
+
+    if (dev->short_sense.key != NO_SENSE) {
+        SPICE_DEBUG("%s, lun:%" G_GUINT32_FORMAT " reported sense: 0x%02x %02x %02x - %s, %s",
+                    __FUNCTION__, req->lun,
+                    dev->short_sense.key, dev->short_sense.asc, dev->short_sense.ascq,
+                    cd_scsi_sense_key_descr(dev->short_sense.key),
+                    dev->short_sense.descr);
+    }
+    memcpy(req->buf, dev->fixed_sense, sizeof(dev->fixed_sense));
+    cd_scsi_dev_sense_reset(dev); /* clear reported sense */
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_cmd_report_luns(CdScsiTarget *st, CdScsiLU *dev,
+                                    CdScsiRequest *req)
+{
+    uint8_t *out_buf = req->buf;
+    uint32_t num_luns = st->num_luns;
+    uint32_t buflen = 8; /* header length */
+    uint32_t lun;
+
+    req->req_len = scsi_cdb_xfer_length(req->cdb, 12);
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    if (req->cdb[0] == 0x01) {
+        /* only well known logical units */
+        num_luns = 0;
+    }
+
+    out_buf[0] = (uint8_t)(num_luns >> 24);
+    out_buf[1] = (uint8_t)(num_luns >> 16);
+    out_buf[2] = (uint8_t)(num_luns >> 8);
+    out_buf[3] = (uint8_t)(num_luns);
+    memset(&out_buf[4], 0, 4);
+
+    if (num_luns > 0) {
+        for (lun = 0; lun < num_luns; lun++) {
+            if (st->units[lun].realized) {
+                out_buf[buflen++] = (uint8_t)(num_luns >> 24);
+                out_buf[buflen++] = (uint8_t)(num_luns >> 16);
+                out_buf[buflen++] = (uint8_t)(num_luns >> 8);
+                out_buf[buflen++] = (uint8_t)(num_luns);
+                memset(&out_buf[buflen], 0, 4);
+                buflen += 4;
+            }
+        }
+    }
+
+    req->in_len = buflen;
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define SCSI_MAX_INQUIRY_LEN        256
+#define SCSI_MAX_MODE_LEN           256
+
+static void cd_scsi_cmd_inquiry_vpd_no_lun(CdScsiLU *dev, CdScsiRequest *req,
+                                           uint32_t perif_qual)
+{
+    uint8_t *outbuf = req->buf;
+    uint8_t page_code = req->cdb[2];
+    uint32_t resp_len = 4;
+
+    outbuf[0] = (perif_qual << 5) | TYPE_ROM;
+    outbuf[1] = page_code ; /* this page */
+    outbuf[2] = 0x00; /* page length MSB */
+    outbuf[3] = 0x00; /* page length LSB - no more data */
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+
+    SPICE_DEBUG("inquiry_vpd, unsupported lun:%" G_GUINT32_FORMAT
+                " perif_qual:0x%x resp_len: %" G_GUINT64_FORMAT,
+                req->lun, perif_qual, req->in_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_cmd_inquiry_vpd(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint8_t page_code = req->cdb[2];
+    int buflen = 4;
+    int start = 4;
+
+    outbuf[0] = TYPE_ROM;
+    outbuf[1] = page_code ; /* this page */
+    outbuf[2] = 0x00; /* page length MSB */
+    outbuf[3] = 0x00; /* page length LSB, to write later */
+
+    switch (page_code) {
+    case 0x00: /* Supported page codes, mandatory */
+    {
+        outbuf[buflen++] = 0x00; // list of supported pages (this page)
+        if (dev->serial) {
+            outbuf[buflen++] = 0x80; // unit serial number
+        }
+        outbuf[buflen++] = 0x83; // device identification
+
+        SPICE_DEBUG("Inquiry EVPD[Supported pages] lun:%" G_GUINT32_FORMAT
+                    " req_len: %" G_GUINT64_FORMAT " resp_len: %d",
+                    req->lun, req->req_len, buflen);
+        break;
+    }
+    case 0x80: /* Device serial number, optional */
+    {
+        int serial_len;
+
+        serial_len = strlen(dev->serial);
+        if (serial_len > 36) {
+            serial_len = 36;
+        }
+        memcpy(outbuf+buflen, dev->serial, serial_len);
+        buflen += serial_len;
+
+        SPICE_DEBUG("Inquiry EVPD[Serial num] lun:%" G_GUINT32_FORMAT
+                    " req_len: %" G_GUINT64_FORMAT " resp_len: %d",
+                    req->lun, req->req_len, buflen);
+        break;
+    }
+    case 0x83: /* Device identification page, mandatory */
+    {
+        int serial_len = strlen(dev->serial);
+        int max_len = 20;
+
+        if (serial_len > max_len) {
+            serial_len = max_len;
+        }
+
+        outbuf[buflen++] = 0x2; // ASCII
+        outbuf[buflen++] = 0;   // not officially assigned
+        outbuf[buflen++] = 0;   // reserved
+        outbuf[buflen++] = serial_len; // length of data following
+
+        memcpy(outbuf+buflen, dev->serial, serial_len);
+        buflen += serial_len;
+
+        SPICE_DEBUG("Inquiry EVPD[Device id] lun:%" G_GUINT32_FORMAT
+                    " req_len: %" G_GUINT64_FORMAT " resp_len: %d",
+                    req->lun, req->req_len, buflen);
+        break;
+    }
+
+    default:
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        SPICE_DEBUG("inquiry_standard, lun:%" G_GUINT32_FORMAT " invalid page_code: %02x",
+                    req->lun, (int)page_code);
+        return;
+    }
+
+    /* done with EVPD */
+    g_assert(buflen - start <= 255);
+    outbuf[3] = buflen - start; /* page length LSB */
+
+    req->in_len = buflen;
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define INQUIRY_STANDARD_LEN_MIN            36
+#define INQUIRY_STANDARD_LEN                96
+#define INQUIRY_STANDARD_LEN_NO_VER         57
+
+#define PERIF_QUALIFIER_CONNECTED           0x00
+#define PERIF_QUALIFIER_NOT_CONNECTED       0x01
+#define PERIF_QUALIFIER_UNSUPPORTED         0x03
+
+#define INQUIRY_REMOVABLE_MEDIUM            0x80
+
+#define INQUIRY_VERSION_NONE                0x00
+#define INQUIRY_VERSION_SPC3                0x05
+
+/* byte 3 */
+#define INQUIRY_RESP_HISUP                  (0x01 << 4)
+#define INQUIRY_RESP_NORM_ACA               (0x01 << 5)
+#define INQUIRY_RESP_DATA_FORMAT_SPC3       0x02
+
+#define INQUIRY_VERSION_DESC_SAM2           0x040
+#define INQUIRY_VERSION_DESC_SPC3           0x300
+#define INQUIRY_VERSION_DESC_MMC3           0x2A0
+#define INQUIRY_VERSION_DESC_SBC2           0x320
+
+static void cd_scsi_cmd_inquiry_standard_no_lun(CdScsiLU *dev, CdScsiRequest *req,
+                                                uint32_t perif_qual)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t resp_len = INQUIRY_STANDARD_LEN_MIN;
+
+    memset(req->buf, 0, INQUIRY_STANDARD_LEN_MIN);
+
+    outbuf[0] = (perif_qual << 5) | TYPE_ROM;
+    outbuf[2] = INQUIRY_VERSION_NONE;
+    outbuf[3] = INQUIRY_RESP_DATA_FORMAT_SPC3;
+
+    outbuf[4] = resp_len - 4; /* additional length, after header */
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+
+    SPICE_DEBUG("inquiry_standard, unsupported lun:%" G_GUINT32_FORMAT " perif_qual:0x%x "
+                "inquiry_len: %" G_GUINT32_FORMAT " resp_len: %" G_GUINT64_FORMAT,
+                req->lun, perif_qual, resp_len, req->in_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_cmd_inquiry_standard(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t resp_len = (dev->claim_version == 0) ? INQUIRY_STANDARD_LEN_NO_VER : INQUIRY_STANDARD_LEN;
+
+    outbuf[0] = (PERIF_QUALIFIER_CONNECTED << 5) | TYPE_ROM;
+    outbuf[1] = (dev->removable) ? INQUIRY_REMOVABLE_MEDIUM : 0;
+    outbuf[2] = (dev->claim_version == 0) ? INQUIRY_VERSION_NONE : INQUIRY_VERSION_SPC3;
+    outbuf[3] = INQUIRY_RESP_NORM_ACA | INQUIRY_RESP_HISUP | INQUIRY_RESP_DATA_FORMAT_SPC3;
+
+    outbuf[4] = resp_len - 4; /* additional length, after header */
+
+    /* (outbuf[6,7] = 0) means also {BQue=0,CmdQue=0} - no queueing at all */
+
+    strpadcpy((char *) &outbuf[8], 8, dev->vendor, ' ');
+    strpadcpy((char *) &outbuf[16], 16, dev->product, ' ');
+    memcpy(&outbuf[32], dev->version, MIN(4, strlen(dev->version)));
+
+    if (dev->claim_version > 0) {
+        /* now supporting only 3 */
+        outbuf[58] = (INQUIRY_VERSION_DESC_SAM2 >> 8) & 0xff;
+        outbuf[59] = INQUIRY_VERSION_DESC_SAM2 & 0xff;
+
+        outbuf[60] = (INQUIRY_VERSION_DESC_SPC3 >> 8) & 0xff;
+        outbuf[61] = INQUIRY_VERSION_DESC_SPC3 & 0xff;
+
+        outbuf[62] = (INQUIRY_VERSION_DESC_MMC3 >> 8) & 0xff;
+        outbuf[63] = INQUIRY_VERSION_DESC_MMC3 & 0xff;
+
+        outbuf[64] = (INQUIRY_VERSION_DESC_SBC2 >> 8) & 0xff;
+        outbuf[65] = INQUIRY_VERSION_DESC_SBC2 & 0xff;
+    }
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+
+    SPICE_DEBUG("inquiry_standard, lun:%" G_GUINT32_FORMAT
+                " inquiry_len: %" G_GUINT32_FORMAT " resp_len: %" G_GUINT64_FORMAT,
+                req->lun, resp_len, req->in_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define CD_INQUIRY_FLAG_EVPD                0x01
+#define CD_INQUIRY_FLAG_CMD_DT              0x02
+
+static void cd_scsi_cmd_inquiry(CdScsiLU *dev, CdScsiRequest *req)
+{
+    gboolean evpd, cmd_data;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    evpd = (req->cdb[1] & CD_INQUIRY_FLAG_EVPD) ? TRUE : FALSE;
+    cmd_data = (req->cdb[1] & CD_INQUIRY_FLAG_CMD_DT) ? TRUE : FALSE;
+
+    if (cmd_data) {
+        SPICE_DEBUG("inquiry, lun:%" G_GUINT32_FORMAT " CmdDT bit set - unsupported, "
+                    "cdb[1]:0x%02x cdb[1]:0x%02x",
+                    req->lun, (int)req->cdb[1], (int)req->cdb[2]);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+
+    req->req_len = req->cdb[4] | (req->cdb[3] << 8);
+    memset(req->buf, 0, req->req_len);
+
+    if (evpd) { /* enable vital product data */
+        cd_scsi_cmd_inquiry_vpd(dev, req);
+    } else { /* standard inquiry data */
+        if (req->cdb[2] != 0) {
+            SPICE_DEBUG("inquiry_standard, lun:%" G_GUINT32_FORMAT " non-zero page code: %02x",
+                        req->lun, (int)req->cdb[2]);
+            cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+            return;
+        }
+        cd_scsi_cmd_inquiry_standard(dev, req);
+    }
+}
+
+static void cd_scsi_cmd_read_capacity(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint32_t last_blk = dev->num_blocks - 1;
+    uint32_t blk_size = dev->block_size;
+    uint32_t *last_blk_out = (uint32_t *)req->buf;
+    uint32_t *blk_size_out = (uint32_t *)(req->buf + 4);
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+    req->req_len = 8;
+
+    *last_blk_out = htobe32(last_blk);
+    *blk_size_out = htobe32(blk_size);
+
+    SPICE_DEBUG("Read capacity, lun:%" G_GUINT32_FORMAT
+                " last_blk: %" G_GUINT32_FORMAT " blk_sz: %" G_GUINT32_FORMAT,
+                req->lun, last_blk, blk_size);
+
+    req->in_len = 8;
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define RDI_TYPE_STANDARD           0 /* Standard Disc Information */
+#define RDI_TYPE_TRACK_RESOURCES    1 /* Track Resources Information */
+#define RDI_TYPE_POW_RESOURCES      2 /* POW Resources Information */
+
+#define RDI_STANDARD_LEN            34
+
+#define RDI_ERAZABLE                (0x01 << 4)
+#define RDI_NON_ERAZABLE            (0x00 << 4)
+
+#define RDI_SESSION_EMPTY           (0x00 << 2)
+#define RDI_SESSION_INCOMPLETE      (0x01 << 2)
+#define RDI_SESSION_DAMAGED         (0x02 << 2)
+#define RDI_SESSION_COMPLETE        (0x03 << 2)
+
+#define RDI_DISC_EMPTY              0x00
+#define RDI_DISC_INCOMPLETE         0x01
+#define RDI_DISC_COMPLETE           0x02
+#define RDI_DISC_RANDOM_WR          0x03
+
+#define RDI_DISC_PMA_TYPE_CD_ROM    0x00
+#define RDI_DISC_PMA_TYPE_CDI       0x10
+#define RDI_DISC_PMA_TYPE_DDCD      0x20
+#define RDI_DISC_PMA_TYPE_UNDEFINED 0xFF
+
+static void cd_scsi_cmd_get_read_disc_information(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t data_type;
+    uint32_t first_track = 1;
+    uint32_t last_track = 1;
+    uint32_t num_sessions = 1;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    data_type = req->cdb[1] & 0x7;
+    if (data_type != RDI_TYPE_STANDARD) {
+        SPICE_DEBUG("read_disc_information, lun:%" G_GUINT32_FORMAT
+                    " unsupported data type: %02x",
+                    req->lun, data_type);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+
+    req->req_len = (req->cdb[7] << 8) | req->cdb[8];
+    req->in_len = (req->req_len < RDI_STANDARD_LEN) ? req->req_len : RDI_STANDARD_LEN;
+
+    memset(outbuf, 0, RDI_STANDARD_LEN);
+    outbuf[1] = RDI_STANDARD_LEN - 2; /* length excluding the counter itself */
+    outbuf[2] = RDI_NON_ERAZABLE | RDI_SESSION_COMPLETE | RDI_DISC_COMPLETE;
+    outbuf[3] = first_track; /* on disk */
+    outbuf[4] = num_sessions & 0xff; /* lsb */
+    outbuf[5] = first_track & 0xff; /* in last sesson, lsb */
+    outbuf[6] = last_track & 0xff; /* in last sesson, lsb */
+    outbuf[8] = RDI_DISC_PMA_TYPE_CD_ROM;
+    outbuf[9] = (num_sessions >> 8) & 0xff; /* msb */
+    outbuf[10] = (first_track >> 8) & 0xff; /* in last sesson, lsb */
+    outbuf[11] = (last_track >> 8) & 0xff; /* in last sesson, lsb */
+
+    SPICE_DEBUG("read_disc_information, lun:%" G_GUINT32_FORMAT " len: %" G_GUINT64_FORMAT,
+                req->lun, req->in_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define RTI_ADDR_TYPE_LBA           0x00
+#define RTI_ADDR_TYPE_TRACK_NUM     0x01
+#define RTI_ADDR_TYPE_SESSION_NUM   0x02
+
+#define RTI_TRACK_NUM_LEAD_IN       0x00
+#define RTI_TRACK_NUM_INVISIBLE     0xff
+
+#define TIB_LEN                     0x36
+
+#define TIB_TRACK_MODE_CD           0x04
+#define TIB_DATA_MODE_ISO_10149     0x01
+
+#define TIB_LRA_VALID               (0x01 << 1)
+
+static void cd_scsi_cmd_get_read_track_information(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t track_size = dev->num_blocks;
+    uint32_t last_addr = track_size - 1;
+    uint32_t track_num = 1;
+    uint32_t session_num = 1;
+    uint32_t addr_type;
+    uint32_t addr_num;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    addr_type = req->cdb[1] & 0x3;
+    addr_num = (req->cdb[2] << 24) | (req->cdb[3] << 16) |
+               (req->cdb[4] << 8) | req->cdb[5];
+
+    switch (addr_type) {
+    case RTI_ADDR_TYPE_LBA:
+        if (addr_num > last_addr) {
+            SPICE_DEBUG("read_track_information, lun:%" G_GUINT32_FORMAT
+                        " addr_type LBA: %" G_GUINT32_FORMAT
+                        " invalid LBA: %" G_GUINT32_FORMAT,
+                        req->lun, addr_type, addr_num);
+            cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+            return;
+        }
+        break;
+    case RTI_ADDR_TYPE_TRACK_NUM:
+        if (addr_num != track_num) {
+            SPICE_DEBUG("read_track_information, lun:%" G_GUINT32_FORMAT
+                        " addr_type track: %" G_GUINT32_FORMAT
+                        " invalid track: %" G_GUINT32_FORMAT,
+                        req->lun, addr_type, addr_num);
+            cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+            return;
+        }
+        break;
+    case RTI_ADDR_TYPE_SESSION_NUM:
+        if (addr_num != session_num) {
+            SPICE_DEBUG("read_track_information, lun:%" G_GUINT32_FORMAT
+                        " addr_type session: %" G_GUINT32_FORMAT
+                        " invalid session: %" G_GUINT32_FORMAT,
+                        req->lun, addr_type, addr_num);
+            cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+            return;
+        }
+        break;
+    default:
+        SPICE_DEBUG("read_track_information, lun:%" G_GUINT32_FORMAT
+                    "invalid addr_type: %" G_GUINT32_FORMAT
+                    " addr_num: %" G_GUINT32_FORMAT,
+                    req->lun, addr_type, addr_num);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+
+    req->req_len = (req->cdb[7] << 8) | req->cdb[8];
+    req->in_len = (req->req_len < TIB_LEN) ? req->req_len : TIB_LEN;
+
+    memset(outbuf, 0, TIB_LEN);
+    outbuf[1] = TIB_LEN - 2;
+    outbuf[2] = session_num;
+    outbuf[3] = track_num;
+    outbuf[5] = TIB_TRACK_MODE_CD & 0x0f;
+    outbuf[6] = TIB_DATA_MODE_ISO_10149 & 0x0f;
+    outbuf[7] = TIB_LRA_VALID;
+
+    /* Track size */
+    outbuf[24] = (track_size >> 24) & 0xff;
+    outbuf[25] = (track_size >> 16) & 0xff;
+    outbuf[26] = (track_size >> 8) & 0xff;
+    outbuf[27] = (track_size) & 0xff;
+
+    /* Last recorded address */
+    outbuf[28] = (last_addr >> 24) & 0xff;
+    outbuf[29] = (last_addr >> 16) & 0xff;
+    outbuf[30] = (last_addr >> 8) & 0xff;
+    outbuf[31] = (last_addr) & 0xff;
+
+    SPICE_DEBUG("read_track_information, lun:%" G_GUINT32_FORMAT
+                "addr_type: %" G_GUINT32_FORMAT " addr_num: %" G_GUINT32_FORMAT,
+                req->lun, addr_type, addr_num);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define READ_TOC_TRACK_DESC_LEN     8
+#define READ_TOC_RESP_LEN           (4 + 2*READ_TOC_TRACK_DESC_LEN)
+
+static void cd_scsi_cmd_read_toc(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t msf, format, track_num;
+    uint32_t last_blk = dev->num_blocks - 1;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    msf = (req->cdb[1] >> 1) & 0x1;
+    format = req->cdb[2] & 0xf;
+    track_num = req->cdb[6];
+
+    req->req_len = (req->cdb[7] << 8) | req->cdb[8];
+    req->in_len = (req->req_len < READ_TOC_RESP_LEN) ? req->req_len : READ_TOC_RESP_LEN;
+
+    memset(outbuf, 0, READ_TOC_RESP_LEN);
+    outbuf[1] = READ_TOC_RESP_LEN - 2; /* length excluding the counter itself */
+    outbuf[2] = 1; /* first track/session */
+    outbuf[3] = 1; /* last track/session */
+
+    outbuf[5] = 0x04; /* Data CD, no Q-subchannel */
+    outbuf[6] = 0x01; /* Track number */
+    outbuf[10] = msf ? 0x02 : 0x0;
+
+    outbuf[13] = 0x04; /* Data CD, no Q-subchannel */
+    outbuf[14] = 0xaa; /* Track number */
+    if (msf) {
+        last_blk = 0xff300000;
+    }
+    outbuf[16] = last_blk >> 24;
+    outbuf[17] = last_blk >> 16;
+    outbuf[18] = last_blk >> 8;
+    outbuf[19] = last_blk;
+
+    SPICE_DEBUG("read_toc, lun:%" G_GUINT32_FORMAT " len: %" G_GUINT64_FORMAT
+                " msf: %x format: 0x%02x track/session: 0x%02x",
+                req->lun, req->in_len, msf, format, track_num);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define CD_MODE_PARAM_6_LEN_HEADER              4
+#define CD_MODE_PARAM_10_LEN_HEADER             8
+
+#define CD_MODE_PAGE_LEN_RW_ERROR               12
+
+static uint32_t cd_scsi_add_mode_page_rw_error_recovery(CdScsiLU *dev, uint8_t *outbuf)
+{
+    uint32_t page_len = CD_MODE_PAGE_LEN_RW_ERROR;
+
+    outbuf[0] = MODE_PAGE_R_W_ERROR;
+    outbuf[1] = CD_MODE_PAGE_LEN_RW_ERROR - 2;
+    outbuf[3] = 1; /* read retry count */
+
+    return page_len;
+}
+
+#define CD_MODE_PAGE_LEN_POWER                  12
+
+static uint32_t cd_scsi_add_mode_page_power_condition(CdScsiLU *dev, uint8_t *outbuf)
+{
+    uint32_t page_len = CD_MODE_PAGE_LEN_POWER;
+
+    outbuf[0] = MODE_PAGE_POWER;
+    outbuf[1] = CD_MODE_PAGE_LEN_POWER - 2;
+
+    return page_len;
+}
+
+#define CD_MODE_PAGE_LEN_FAULT_FAIL             12
+#define CD_MODE_PAGE_FAULT_FAIL_FLAG_PERF       0x80
+
+static uint32_t cd_scsi_add_mode_page_fault_reporting(CdScsiLU *dev, uint8_t *outbuf)
+{
+    uint32_t page_len = CD_MODE_PAGE_LEN_FAULT_FAIL;
+
+    outbuf[0] = MODE_PAGE_FAULT_FAIL;
+    outbuf[1] = CD_MODE_PAGE_LEN_FAULT_FAIL - 2;
+    outbuf[2] |= CD_MODE_PAGE_FAULT_FAIL_FLAG_PERF;
+
+    return page_len;
+}
+
+#define CD_MODE_PAGE_LEN_CAPS_MECH_STATUS_RO    26
+/* byte 2 */
+#define CD_MODE_PAGE_CAPS_CD_R_READ             0x01
+#define CD_MODE_PAGE_CAPS_CD_RW_READ            (0x01 << 1)
+#define CD_MODE_PAGE_CAPS_DVD_ROM_READ          (0x01 << 3)
+#define CD_MODE_PAGE_CAPS_DVD_R_READ            (0x01 << 4)
+#define CD_MODE_PAGE_CAPS_DVD_RAM_READ          (0x01 << 5)
+/* byte 6 */
+#define CD_MODE_PAGE_CAPS_LOCK_SUPPORT          (0x01)
+#define CD_MODE_PAGE_CAPS_LOCK_STATE            (0x01 << 1)
+#define CD_MODE_PAGE_CAPS_PREVENT_JUMPER        (0x01 << 2)
+#define CD_MODE_PAGE_CAPS_EJECT                 (0x01 << 3)
+#define CD_MODE_PAGE_CAPS_LOADING_TRAY          (0x01 << 5)
+
+static uint32_t cd_scsi_add_mode_page_caps_mech_status(CdScsiLU *dev, uint8_t *outbuf)
+{
+    uint32_t page_len = CD_MODE_PAGE_LEN_CAPS_MECH_STATUS_RO; /* no write */
+
+    outbuf[0] = MODE_PAGE_CAPS_MECH_STATUS;
+    outbuf[1] = page_len;
+    outbuf[2] = CD_MODE_PAGE_CAPS_CD_R_READ | CD_MODE_PAGE_CAPS_CD_RW_READ |
+                CD_MODE_PAGE_CAPS_DVD_ROM_READ | CD_MODE_PAGE_CAPS_DVD_R_READ |
+                CD_MODE_PAGE_CAPS_DVD_RAM_READ;
+    outbuf[6] = CD_MODE_PAGE_CAPS_LOADING_TRAY | CD_MODE_PAGE_CAPS_EJECT | CD_MODE_PAGE_CAPS_LOCK_SUPPORT;
+    if (dev->prevent_media_removal) {
+        outbuf[6] |= CD_MODE_PAGE_CAPS_LOCK_STATE;
+    }
+
+    return page_len;
+}
+
+static void cd_scsi_cmd_mode_sense_10(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    int long_lba, dbd, page, sub_page, pc;
+    uint32_t resp_len = CD_MODE_PARAM_10_LEN_HEADER;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    long_lba = (req->cdb[1] >> 4) & 0x1;
+    dbd = (req->cdb[1] >> 3) & 0x1;
+    page = req->cdb[2] & 0x3f;
+    pc = req->cdb[2] >> 6;
+    sub_page = req->cdb[3] & 0xf;
+
+    req->req_len = (req->cdb[7] << 8) | req->cdb[8];
+
+    memset(outbuf, 0, req->req_len);
+    outbuf[2] =  0; /* medium type */
+
+    switch (page) {
+    case MODE_PAGE_R_W_ERROR:
+        /* Read/Write Error Recovery */
+        resp_len += cd_scsi_add_mode_page_rw_error_recovery(dev, outbuf + resp_len);
+        break;
+    case MODE_PAGE_POWER:
+        /* Power Condistions */
+        resp_len += cd_scsi_add_mode_page_power_condition(dev, outbuf + resp_len);
+        break;
+    case MODE_PAGE_FAULT_FAIL:
+        /* Fault / Failure Reporting Control */
+        resp_len += cd_scsi_add_mode_page_fault_reporting(dev, outbuf + resp_len);
+        break;
+    case MODE_PAGE_CAPS_MECH_STATUS:
+        resp_len += cd_scsi_add_mode_page_caps_mech_status(dev, outbuf + resp_len);
+        break;
+
+    /* not implemented */
+    case MODE_PAGE_WRITE_PARAMETER: /* Writer Parameters */
+    case MODE_PAGE_MRW:
+    case MODE_PAGE_MRW_VENDOR: /* MRW (Mount Rainier Re-writable Disks */
+    case MODE_PAGE_CD_DEVICE: /* CD Device parameters */
+    case MODE_PAGE_TO_PROTECT: /* Time-out and Protect */
+    default:
+        SPICE_DEBUG("mode_sense_10, lun:%" G_GUINT32_FORMAT
+                    " page 0x%x not implemented",
+                    req->lun, (unsigned)page);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+
+    outbuf[0] = ((resp_len - 2) >> 8) & 0xff;
+    outbuf[1] = (resp_len - 2) & 0xff;
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+
+    SPICE_DEBUG("mode_sense_10, lun:%" G_GUINT32_FORMAT
+                " long_lba %d, dbd %d, page %d, sub_page %d, pc %d; "
+                "resp_len %" G_GUINT32_FORMAT,
+                req->lun, long_lba, dbd, page, sub_page, pc, resp_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_cmd_mode_select_6(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *block_desc_data, *mode_data;
+    uint32_t page_format, save_pages, list_len; /* cdb */
+    uint32_t num_blocks = 0, block_len = 0; /* block descriptor */
+    uint32_t mode_len, medium_type, dev_param, block_desc_len; /* mode param header */
+    uint32_t page_num = 0, page_len = 0; /* mode page */
+
+    page_format = (req->cdb[1] >> 4) & 0x1;
+    save_pages = req->cdb[1] & 0x1;
+    list_len = req->cdb[4];
+
+    if (list_len > req->buf_len) {
+        SPICE_DEBUG("mode_select_6, lun:%" G_GUINT32_FORMAT
+                    " pf:%" G_GUINT32_FORMAT " sp:%" G_GUINT32_FORMAT
+                    " list_len:%" G_GUINT32_FORMAT " exceeds data_len:%" G_GUINT32_FORMAT,
+                    req->lun, page_format, save_pages, list_len, req->buf_len);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_PARAM_LEN);
+        return;
+    }
+
+    mode_len = req->buf[0];
+    medium_type = req->buf[1];
+    dev_param = req->buf[2];
+    block_desc_len = req->buf[3];
+
+    if (block_desc_len) {
+        block_desc_data = &req->buf[CD_MODE_PARAM_6_LEN_HEADER];
+        num_blocks = (block_desc_data[3] << 16) | (block_desc_data[2] << 8) | block_desc_data[3];
+        block_len = (block_desc_data[5] << 16) | (block_desc_data[6] << 8) | block_desc_data[7];
+    }
+
+    if (mode_len) {
+        mode_data = &req->buf[CD_MODE_PARAM_6_LEN_HEADER];
+        if (block_desc_len) {
+            mode_data += block_desc_len;
+        }
+        page_num = mode_data[0] & 0x3f;
+        page_len = mode_data[1];
+    }
+
+    SPICE_DEBUG("mode_select_6, lun:%" G_GUINT32_FORMAT
+                " pf:%" G_GUINT32_FORMAT " sp:%" G_GUINT32_FORMAT
+                " list_len:%" G_GUINT32_FORMAT " data_len:%" G_GUINT32_FORMAT
+                " mode_len:%" G_GUINT32_FORMAT " medium:%" G_GUINT32_FORMAT
+                " dev_param:%" G_GUINT32_FORMAT " blk_desc_len:%" G_GUINT32_FORMAT
+                " num_blocks:%" G_GUINT32_FORMAT " block_len:%" G_GUINT32_FORMAT
+                " page_num:%" G_GUINT32_FORMAT " page_len:%" G_GUINT32_FORMAT,
+                req->lun, page_format, save_pages, list_len, req->buf_len,
+                mode_len, medium_type, dev_param, block_desc_len,
+                num_blocks, block_len,
+                page_num, page_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_cmd_mode_select_10(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint32_t page_format, save_pages, list_len;
+
+    page_format = (req->cdb[1] >> 4) & 0x1;
+    save_pages = req->cdb[1] & 0x1;
+    list_len = (req->cdb[7] << 8) | req->cdb[8];
+
+    if (list_len > req->buf_len) {
+        SPICE_DEBUG("mode_select_10, lun:%" G_GUINT32_FORMAT
+                    " pf:%" G_GUINT32_FORMAT " sp:%" G_GUINT32_FORMAT
+                    " list_len:%" G_GUINT32_FORMAT " exceeds data_len:%" G_GUINT32_FORMAT,
+                    req->lun, page_format, save_pages, list_len, req->buf_len);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_PARAM_LEN);
+        return;
+    }
+
+    SPICE_DEBUG("mode_select_10, lun:%" G_GUINT32_FORMAT
+                " pf:%" G_GUINT32_FORMAT " sp:%" G_GUINT32_FORMAT
+                " list_len:%" G_GUINT32_FORMAT " data_len:%" G_GUINT32_FORMAT,
+                req->lun, page_format, save_pages, list_len, req->buf_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define CD_FEATURE_HEADER_LEN               8
+#define CD_FEATURE_DESC_LEN                 4
+
+#define CD_PROFILE_DESC_LEN                 4
+#define CD_PROFILE_CURRENT                  0x01
+
+/* Profiles List */
+#define CD_FEATURE_NUM_PROFILES_LIST        0x00
+/* Core - Basic Functionality */
+#define CD_FEATURE_NUM_CORE                 0x01
+/* Morphing - The device changes its behavior due to external events */
+#define CD_FEATURE_NUM_MORPH                0x02
+/* Removable Medium - The medium may be removed from the device */
+#define CD_FEATURE_NUM_REMOVABLE            0x03
+/* Random Readable - PP=1 Read ability for storage devices with random addressing */
+#define CD_FEATURE_NUM_RANDOM_READ          0x10
+/* CD Read - The ability to read CD specific structures */
+#define CD_FEATURE_NUM_CD_READ              0x1E
+/* DVD Read - The ability to read DVD specific structures */
+#define CD_FEATURE_NUM_DVD_READ             0x1F
+/* Power Management - Initiator and device directed power management */
+#define CD_FEATURE_NUM_POWER_MNGT           0x100
+/* Timeout */
+#define CD_FEATURE_NUM_TIMEOUT              0x105
+
+#define CD_FEATURE_REQ_ALL                  0
+#define CD_FEATURE_REQ_CURRENT              1
+#define CD_FEATURE_REQ_SINGLE               2
+
+#define CD_FEATURE_CURRENT                  0x01
+#define CD_FEATURE_PERSISTENT               0x02
+
+#define CD_FEATURE_VERSION_1                (0x01 << 2)
+
+#define CD_FEATURE_PHYS_IF_SCSI             0x01
+
+#define CD_FEATURE_REMOVABLE_LOADING_TRAY   (0x01 << 5)
+#define CD_FEATURE_REMOVABLE_EJECT          (0x01 << 3)
+#define CD_FEATURE_REMOVABLE_NO_PRVNT_JMPR  (0x01 << 2)
+#define CD_FEATURE_REMOVABLE_LOCK           (0x01)
+
+static gboolean cd_scsi_feature_reportable(uint32_t feature, uint32_t start_feature, uint32_t req_type)
+{
+    return (req_type == CD_FEATURE_REQ_SINGLE && start_feature == feature) ||
+           (feature >= start_feature);
+}
+
+static uint32_t cd_scsi_add_feature_profiles_list(CdScsiLU *dev, uint8_t *outbuf,
+                                                  uint32_t start_feature, uint32_t req_type)
+{
+    uint8_t *profile = outbuf + CD_FEATURE_DESC_LEN;
+    uint32_t feature_len = CD_FEATURE_DESC_LEN;
+    uint32_t add_len, profile_num;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_PROFILES_LIST, start_feature, req_type)) {
+        return 0;
+    }
+    /* feature descriptor header */
+    outbuf[0] = (CD_FEATURE_NUM_PROFILES_LIST >> 8) & 0xff;
+    outbuf[1] = CD_FEATURE_NUM_PROFILES_LIST & 0xff;
+    outbuf[2] = CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+
+    /* DVD-ROM profile descriptor */
+    add_len = CD_PROFILE_DESC_LEN; /* start with single profile, add later */
+    profile_num = MMC_PROFILE_DVD_ROM;
+
+    profile[0] = (profile_num >> 8) & 0xff; /* feature code */
+    profile[1] = profile_num & 0xff;
+    profile[2] = (!dev->cd_rom) ? CD_PROFILE_CURRENT : 0;
+
+    /* next profile */
+    add_len += CD_PROFILE_DESC_LEN;
+    profile += CD_PROFILE_DESC_LEN;
+
+    /* CD-ROM profile descriptor */
+    profile_num = MMC_PROFILE_CD_ROM;
+    profile[0] = (profile_num >> 8) & 0xff;
+    profile[1] = profile_num & 0xff;
+    profile[2] = dev->cd_rom ? CD_PROFILE_CURRENT : 0;
+
+    outbuf[3] = add_len;
+    feature_len += add_len;
+
+    return feature_len;
+}
+
+#define CD_FEATURE_CORE_PHYS_PROFILE_LEN    4
+
+static uint32_t cd_scsi_add_feature_core(CdScsiLU *dev, uint8_t *outbuf,
+                                         uint32_t start_feature, uint32_t req_type)
+{
+    uint8_t *profile = outbuf + CD_FEATURE_DESC_LEN;
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_CORE_PHYS_PROFILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_CORE, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[0] = (CD_FEATURE_NUM_CORE >> 8) & 0xff;
+    outbuf[1] = CD_FEATURE_NUM_CORE & 0xff;
+    outbuf[2] = CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_CORE_PHYS_PROFILE_LEN;
+
+    profile[3] = CD_FEATURE_PHYS_IF_SCSI;
+
+    return feature_len;
+}
+
+#define CD_FEATURE_MORPH_PROGILE_LEN    4
+#define CD_FEATURE_MORPH_ASYNC_EVENTS   0x01
+
+static uint32_t cd_scsi_add_feature_morph(CdScsiLU *dev, uint8_t *outbuf,
+                                          uint32_t start_feature, uint32_t req_type)
+{
+    uint8_t *profile = outbuf + CD_FEATURE_DESC_LEN;
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_MORPH_PROGILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_MORPH, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[1] = CD_FEATURE_NUM_MORPH;
+    outbuf[2] = CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_MORPH_PROGILE_LEN;
+
+    profile[0] = CD_FEATURE_MORPH_ASYNC_EVENTS;
+
+    return feature_len;
+}
+
+#define CD_FEATURE_REMOVABLE_PROFILE_LEN    4
+
+static uint32_t cd_scsi_add_feature_removable(CdScsiLU *dev, uint8_t *outbuf,
+                                              uint32_t start_feature, uint32_t req_type)
+{
+    uint8_t *profile = outbuf + CD_FEATURE_DESC_LEN;
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_REMOVABLE_PROFILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_REMOVABLE, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[1] = CD_FEATURE_NUM_REMOVABLE;
+    outbuf[2] = CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_REMOVABLE_PROFILE_LEN;
+
+    profile[0] = CD_FEATURE_REMOVABLE_NO_PRVNT_JMPR;
+    if (dev->removable) {
+        profile[0] |= (CD_FEATURE_REMOVABLE_LOADING_TRAY | CD_FEATURE_REMOVABLE_EJECT);
+    }
+
+    return feature_len;
+}
+
+#define CD_FEATURE_RANDOM_READ_PROFILE_LEN    8
+
+static uint32_t cd_scsi_add_feature_random_read(CdScsiLU *dev, uint8_t *outbuf,
+                                                uint32_t start_feature, uint32_t req_type)
+{
+    uint8_t *profile = outbuf + CD_FEATURE_DESC_LEN;
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_RANDOM_READ_PROFILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_RANDOM_READ, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[0] = (CD_FEATURE_NUM_RANDOM_READ >> 8) & 0xff;
+    outbuf[1] = CD_FEATURE_NUM_RANDOM_READ & 0xff;
+    outbuf[2] = CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_RANDOM_READ_PROFILE_LEN;
+
+    profile[0] = (dev->block_size >> 24) & 0xff;
+    profile[1] = (dev->block_size >> 16) & 0xff;
+    profile[2] = (dev->block_size >> 8) & 0xff;
+    profile[3] = (dev->block_size) & 0xff;
+    profile[5] = (dev->cd_rom) ? 0x01 : 0x10; /* logical blocks per readable unit */
+
+    return feature_len;
+}
+
+#define CD_FEATURE_CD_READ_PROFILE_LEN    4
+
+static uint32_t cd_scsi_add_feature_cd_read(CdScsiLU *dev, uint8_t *outbuf,
+                                            uint32_t start_feature, uint32_t req_type)
+{
+    uint8_t *profile = outbuf + CD_FEATURE_DESC_LEN;
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_CD_READ_PROFILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_CD_READ, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[0] = (CD_FEATURE_NUM_CD_READ >> 8) & 0xff;
+    outbuf[1] = (CD_FEATURE_NUM_CD_READ) & 0xff;
+    outbuf[2] = CD_FEATURE_VERSION_1 | CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_CD_READ_PROFILE_LEN;
+
+    profile[0] = 0; /* C2 Errors, CD-Text not supporte */
+
+    return feature_len;
+}
+
+#define CD_FEATURE_DVD_READ_PROFILE_LEN    0
+
+static uint32_t cd_scsi_add_feature_dvd_read(CdScsiLU *dev, uint8_t *outbuf,
+                                             uint32_t start_feature, uint32_t req_type)
+{
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_DVD_READ_PROFILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_CD_READ, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[0] = (CD_FEATURE_NUM_DVD_READ >> 8) & 0xff;
+    outbuf[1] = (CD_FEATURE_NUM_DVD_READ) & 0xff;
+    outbuf[2] = CD_FEATURE_VERSION_1 | CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_DVD_READ_PROFILE_LEN;
+
+    return feature_len;
+}
+
+#define CD_FEATURE_POWER_MNGT_PROFILE_LEN    0
+
+static uint32_t cd_scsi_add_feature_power_mgmt(CdScsiLU *dev, uint8_t *outbuf,
+                                               uint32_t start_feature, uint32_t req_type)
+{
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_POWER_MNGT_PROFILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_POWER_MNGT, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[0] = (CD_FEATURE_NUM_POWER_MNGT >> 8) & 0xff;
+    outbuf[1] = (CD_FEATURE_NUM_POWER_MNGT) & 0xff;
+    outbuf[2] = CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_POWER_MNGT_PROFILE_LEN;
+
+    return feature_len;
+}
+
+#define CD_FEATURE_TIMEOUT_PROFILE_LEN    0
+
+static uint32_t cd_scsi_add_feature_timeout(CdScsiLU *dev, uint8_t *outbuf,
+                                            uint32_t start_feature, uint32_t req_type)
+{
+    uint32_t feature_len = CD_FEATURE_DESC_LEN + CD_FEATURE_TIMEOUT_PROFILE_LEN;
+
+    if (!cd_scsi_feature_reportable(CD_FEATURE_NUM_TIMEOUT, start_feature, req_type)) {
+        return 0;
+    }
+    outbuf[0] = (CD_FEATURE_NUM_TIMEOUT >> 8) & 0xff;
+    outbuf[1] = CD_FEATURE_NUM_TIMEOUT & 0xff;
+    outbuf[2] = CD_FEATURE_PERSISTENT | CD_FEATURE_CURRENT;
+    outbuf[3] = CD_FEATURE_TIMEOUT_PROFILE_LEN;
+
+    return feature_len;
+}
+
+static void cd_scsi_cmd_get_configuration(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t profile_num = (!dev->cd_rom) ? MMC_PROFILE_DVD_ROM : MMC_PROFILE_CD_ROM;
+    uint32_t req_type, start_feature, resp_len;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    req_type = req->cdb[1] & 0x3;
+    start_feature = (req->cdb[2] << 8) | req->cdb[3];
+    req->req_len = (req->cdb[7] << 8) | req->cdb[8];
+
+    memset(outbuf, 0, req->req_len);
+
+    /* at least Feature Header should be present, to be filled later */
+    resp_len = CD_FEATURE_HEADER_LEN;
+
+    switch (req_type) {
+    case CD_FEATURE_REQ_ALL:
+    case CD_FEATURE_REQ_CURRENT:
+        resp_len += cd_scsi_add_feature_profiles_list(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_core(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_morph(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_removable(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_random_read(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_cd_read(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_dvd_read(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_power_mgmt(dev, outbuf + resp_len, start_feature, req_type);
+        resp_len += cd_scsi_add_feature_timeout(dev, outbuf + resp_len, start_feature, req_type);
+        break;
+    case CD_FEATURE_REQ_SINGLE:
+        switch (start_feature) {
+        case CD_FEATURE_NUM_CORE:
+            resp_len += cd_scsi_add_feature_core(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        case CD_FEATURE_NUM_MORPH:
+            resp_len += cd_scsi_add_feature_morph(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        case CD_FEATURE_NUM_REMOVABLE:
+            resp_len += cd_scsi_add_feature_removable(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        case CD_FEATURE_NUM_RANDOM_READ:
+            resp_len += cd_scsi_add_feature_random_read(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        case CD_FEATURE_NUM_CD_READ:
+            resp_len += cd_scsi_add_feature_cd_read(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        case CD_FEATURE_NUM_DVD_READ:
+            resp_len += cd_scsi_add_feature_dvd_read(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        case CD_FEATURE_NUM_POWER_MNGT:
+            resp_len += cd_scsi_add_feature_power_mgmt(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        case CD_FEATURE_NUM_TIMEOUT:
+            resp_len += cd_scsi_add_feature_timeout(dev, outbuf + resp_len, start_feature, req_type);
+            break;
+        default:
+            break;
+        }
+        break;
+
+    default:
+        SPICE_DEBUG("get_configuration, lun:%" G_GUINT32_FORMAT
+                    " invalid rt:%" G_GUINT32_FORMAT " start_f:%" G_GUINT32_FORMAT,
+                    req->lun, req_type, start_feature);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+
+    /* set total data len */
+    outbuf[0] = (resp_len >> 24) & 0xff;
+    outbuf[1] = (resp_len >> 16) & 0xff;
+    outbuf[2] = (resp_len >> 8) & 0xff;
+    outbuf[3] = resp_len & 0xff;
+
+    /* report current profile num */
+    outbuf[6] = (profile_num >> 8) & 0xff;
+    outbuf[7] = profile_num & 0xff;
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+
+    SPICE_DEBUG("get_configuration, lun:%" G_GUINT32_FORMAT
+                " rt:%" G_GUINT32_FORMAT " start_f:%" G_GUINT32_FORMAT
+                " resp_len:%" G_GUINT32_FORMAT,
+                req->lun, req_type, start_feature, resp_len);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define CD_GET_EVENT_STATUS_IMMED            0x01
+
+#define CD_GET_EVENT_HEADER_NO_EVENT_AVAIL  (0x01 << 7)
+#define CD_GET_EVENT_HEADER_LEN             4
+
+#define CD_GET_EVENT_CLASS_NONE             (0x00)
+#define CD_GET_EVENT_CLASS_OPER_CHANGE      (0x01)
+#define CD_GET_EVENT_CLASS_POWER_MGMT       (0x02)
+#define CD_GET_EVENT_CLASS_EXTERNAL_REQ     (0x03)
+#define CD_GET_EVENT_CLASS_MEDIA            (0x04)
+#define CD_GET_EVENT_CLASS_MULTI_INITIATOR  (0x05)
+#define CD_GET_EVENT_CLASS_DEV_BUSY         (0x06)
+
+#define CD_GET_EVENT_LEN_MEDIA              4
+
+#define CD_MEDIA_STATUS_MEDIA_PRESENT       0x1
+#define CD_MEDIA_STATUS_TRAY_OPEN           0x2
+
+static uint32_t cd_scsi_cmd_get_event_resp_add_media(CdScsiLU *dev, uint8_t *outbuf)
+{
+    outbuf[0] = (uint8_t)dev->media_event & 0x0f;
+    outbuf[1] = (uint8_t)((dev->loaded ? 0 : CD_MEDIA_STATUS_TRAY_OPEN) |
+                          (dev->stream != NULL ? CD_MEDIA_STATUS_MEDIA_PRESENT : 0));
+
+    dev->media_event = CD_MEDIA_EVENT_NO_CHANGE; /* reset the event */
+    return CD_GET_EVENT_LEN_MEDIA;
+}
+
+#define CD_GET_EVENT_LEN_POWER              4
+
+#define CD_POWER_STATUS_ACTIVE              0x1
+#define CD_POWER_STATUS_IDLE                0x2
+
+static uint32_t cd_scsi_cmd_get_event_resp_add_power(CdScsiLU *dev, uint8_t *outbuf)
+{
+    outbuf[0] = (uint8_t)dev->power_event & 0x0f;
+    outbuf[1] = (uint8_t)((dev->power_cond == CD_SCSI_POWER_ACTIVE) ?
+                           CD_POWER_STATUS_ACTIVE : CD_POWER_STATUS_IDLE);
+
+    dev->power_event = CD_POWER_EVENT_NO_CHANGE; /* reset the event */
+    return CD_GET_EVENT_LEN_POWER;
+}
+
+static void cd_scsi_cmd_get_event_status_notification(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t resp_len = CD_GET_EVENT_HEADER_LEN;
+    const uint32_t power_class_mask = (0x01 << CD_GET_EVENT_CLASS_POWER_MGMT);
+    const uint32_t media_class_mask = (0x01 << CD_GET_EVENT_CLASS_MEDIA);
+    uint32_t classes_supported =  power_class_mask | media_class_mask;
+    uint32_t immed, classes_requested;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    immed = req->cdb[1] & CD_GET_EVENT_STATUS_IMMED;
+    classes_requested = req->cdb[4];
+    req->req_len = (req->cdb[7] << 8) | req->cdb[8];
+
+    if (!immed) {
+        SPICE_DEBUG("get_event_status_notification, lun:%" G_GUINT32_FORMAT
+                " imm:0 class_req:%02x, Non-immediate (async) mode unsupported",
+                req->lun, classes_requested);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+
+    memset(outbuf, 0, req->req_len);
+    if ((classes_supported & classes_requested) != 0) {
+        if (classes_requested & power_class_mask) {
+            outbuf[2] = CD_GET_EVENT_CLASS_POWER_MGMT;
+            outbuf[3] = (uint8_t)power_class_mask;
+
+            SPICE_DEBUG("get_event_status_notification, lun:%" G_GUINT32_FORMAT
+                        " imm:%" G_GUINT32_FORMAT " class_req:0x%02x class_sup:0x%02x"
+                        " power_event:0x%02x power_cond:0x%02x",
+                        req->lun, immed, classes_requested, classes_supported,
+                        dev->power_event, dev->power_cond);
+
+            resp_len += cd_scsi_cmd_get_event_resp_add_power(dev, outbuf + resp_len);
+        } else if (classes_requested & media_class_mask) {
+            outbuf[2] = CD_GET_EVENT_CLASS_MEDIA;
+            outbuf[3] = (uint8_t)media_class_mask;
+
+            SPICE_DEBUG("get_event_status_notification, lun:%" G_GUINT32_FORMAT
+                        " imm:%" G_GUINT32_FORMAT " class_req:0x%02x class_sup:0x%02x"
+                        " media_event:0x%02x loaded: %d",
+                        req->lun, immed, classes_requested, classes_supported,
+                        dev->media_event, dev->loaded);
+
+            resp_len += cd_scsi_cmd_get_event_resp_add_media(dev, outbuf + resp_len);
+        }
+    } else {
+        outbuf[2] = CD_GET_EVENT_HEADER_NO_EVENT_AVAIL | CD_GET_EVENT_CLASS_NONE;
+
+        SPICE_DEBUG("get_event_status_notification, lun:%" G_GUINT32_FORMAT
+                        " imm:%" G_GUINT32_FORMAT " class_req:0x%02x class_sup:0x%02x"
+                        " none of requested events supported",
+                        req->lun, immed, classes_requested, classes_supported);
+    }
+    outbuf[1] = (uint8_t)(resp_len - 2); /* Event Data Length LSB, length excluding the field itself */
+    outbuf[3] = (uint8_t)classes_supported;
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define CD_EXT_REQ_EVENT_FORMAT_NO_CHG          0x00
+#define CD_EXT_REQ_EVENT_FORMAT_LU_KEY_DOWN     0x01
+#define CD_EXT_REQ_EVENT_FORMAT_LU_KEY_UP       0x02
+#define CD_EXT_REQ_EVENT_FORMAT_REQ_NOTIFY      0x03
+
+#define CD_EXT_REQ_STATUS_READY                 0x00
+#define CD_EXT_REQ_STATUS_OTHER_PREVENT         0x01
+
+#define CD_EXT_REQ_CODE_NO_REQUEST              0x00
+#define CD_EXT_REQ_CODE_OVERRUN                 0x01
+#define CD_EXT_REQ_CODE_PLAY                    0x101
+#define CD_EXT_REQ_CODE_REWIND                  0x102
+#define CD_EXT_REQ_CODE_FAST_FW                 0x103
+#define CD_EXT_REQ_CODE_PAUSE                   0x104
+#define CD_EXT_REQ_CODE_STOP                    0x106
+#define CD_EXT_REQ_CODE_ASCII_BASE              0x200 /* SCSII value is LSB */
+
+static void cd_scsi_cmd_send_event(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *param, *event;
+    uint32_t immed, param_list_len;
+    uint32_t event_param_len, notification_class;
+    uint32_t ext_req_event, ext_req_status, pers_prevent, ext_req_code;
+
+    req->xfer_dir = SCSI_XFER_TO_DEV;
+
+    immed = req->cdb[1] & 0x01;
+    param_list_len = (req->cdb[8] << 8) | req->cdb[9];
+
+    if (req->buf_len < param_list_len) {
+        SPICE_DEBUG("send_event, lun:%" G_GUINT32_FORMAT
+                    " invalid param list len:0x%x, buf_len:0x%x",
+                    req->lun, param_list_len, req->buf_len);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_PARAM_LEN);
+        return;
+    }
+    param = req->buf;
+    event_param_len = (param[0] << 8) | param[1];
+
+    notification_class = param[2] & 0x07;
+    if (notification_class != CD_GET_EVENT_CLASS_EXTERNAL_REQ) {
+        SPICE_DEBUG("send_event, lun:%" G_GUINT32_FORMAT
+                    " invalid notification class:0x%x",
+                    req->lun, notification_class);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+
+    event = param + CD_GET_EVENT_HEADER_LEN;
+    ext_req_event = event[0] & 0xff;
+    ext_req_status = event[1] & 0x0f;
+    pers_prevent = event[1] & 0x80;
+    ext_req_code = (event[2] << 8) | event[3];
+
+    SPICE_DEBUG("send_event, lun:0x%" G_GUINT32_FORMAT
+                " immed:%" G_GUINT32_FORMAT " param_len:%" G_GUINT32_FORMAT
+                " ext_req_event:0x%x ext_req_status:0x%x"
+                " pers_prevent:0x%x ext_req_code:0x%x",
+                req->lun, immed, event_param_len, ext_req_event,
+                ext_req_status, pers_prevent, ext_req_code);
+
+    /* ToDo: process the event */
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define CD_MEDIUM_REMOVAL_REQ_ALLOW                 0x00
+#define CD_MEDIUM_REMOVAL_REQ_PREVENT               0x01
+#define CD_MEDIUM_REMOVAL_REQ_ALLOW_CHANGER         0x02
+#define CD_MEDIUM_REMOVAL_REQ_PREVENT_CHANGER       0x03
+
+static void cd_scsi_cmd_allow_medium_removal(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint32_t prevent;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    prevent = req->cdb[4] & 0x03;
+    dev->prevent_media_removal = (prevent == CD_MEDIUM_REMOVAL_REQ_PREVENT ||
+                                  prevent == CD_MEDIUM_REMOVAL_REQ_PREVENT_CHANGER);
+    req->in_len = 0;
+
+    SPICE_DEBUG("allow_medium_removal, lun:%" G_GUINT32_FORMAT " prevent field::0x%02x flag:%d",
+                req->lun, prevent, dev->prevent_media_removal);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_cmd_report_key(CdScsiLU *dev, CdScsiRequest *req)
+{
+    SPICE_DEBUG("report_key - content protection unsupported, lun:%" G_GUINT32_FORMAT, req->lun);
+    req->xfer_dir = SCSI_XFER_NONE;
+    cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_OPCODE);
+}
+
+static void cd_scsi_cmd_send_key(CdScsiLU *dev, CdScsiRequest *req)
+{
+    SPICE_DEBUG("send_key - content protection unsupported, lun:%" G_GUINT32_FORMAT, req->lun);
+    req->xfer_dir = SCSI_XFER_NONE;
+    cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_OPCODE);
+}
+
+/* byte 1 */
+#define CD_START_STOP_FLAG_IMMED                    0x01
+
+/* byte 4 */
+#define CD_START_STOP_FLAG_START                    0x01
+#define CD_START_STOP_FLAG_LOEJ                     0x02
+
+/* POWER CONDITION field values */
+#define CD_START_STOP_POWER_COND_START_VALID        0x00
+#define CD_START_STOP_POWER_COND_ACTIVE             0x01
+#define CD_START_STOP_POWER_COND_IDLE               0x02
+#define CD_START_STOP_POWER_COND_STANDBY            0x03
+#define CD_START_STOP_POWER_COND_LU_CONTROL         0x07
+#define CD_START_STOP_POWER_COND_FORCE_IDLE_0       0x0a
+#define CD_START_STOP_POWER_COND_FORCE_STANDBY_0    0x0b
+
+static inline const char *cd_scsi_start_stop_power_cond_name(uint32_t power_cond)
+{
+    switch (power_cond) {
+    case CD_START_STOP_POWER_COND_START_VALID:
+        return "START_VALID";
+    case CD_START_STOP_POWER_COND_ACTIVE:
+        return "ACTIVE";
+    case CD_START_STOP_POWER_COND_IDLE:
+        return "IDLE";
+    case CD_START_STOP_POWER_COND_STANDBY:
+        return "STANDBY";
+    case CD_START_STOP_POWER_COND_LU_CONTROL:
+        return "LU_CONTROL";
+    case CD_START_STOP_POWER_COND_FORCE_IDLE_0:
+        return "FORCE_IDLE_0";
+    case CD_START_STOP_POWER_COND_FORCE_STANDBY_0:
+        return "FORCE_STANDBY_0";
+    default:
+        return "RESERVED";
+    }
+}
+
+static void cd_scsi_cmd_start_stop_unit(CdScsiLU *dev, CdScsiRequest *req)
+{
+    gboolean immed, start, load_eject;
+    uint32_t power_cond;
+
+    req->xfer_dir = SCSI_XFER_NONE;
+    req->in_len = 0;
+
+    immed = (req->cdb[1] & CD_START_STOP_FLAG_IMMED) ? TRUE : FALSE;
+    start = (req->cdb[4] & CD_START_STOP_FLAG_START) ? TRUE : FALSE;
+    load_eject = (req->cdb[4] & CD_START_STOP_FLAG_LOEJ) ? TRUE : FALSE;
+    power_cond = req->cdb[4] >> 4;
+
+    SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT
+                " immed:%d start:%d load_eject:%d power_cond:0x%x(%s)",
+                req->lun, immed, start, load_eject, power_cond,
+                cd_scsi_start_stop_power_cond_name(power_cond));
+
+    switch (power_cond) {
+    case CD_START_STOP_POWER_COND_START_VALID:
+        if (!start) { /* stop the unit */
+            if (load_eject) { /* eject medium */
+                if (dev->prevent_media_removal) {
+                    SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT
+                                " prevent_media_removal set, eject failed", req->lun);
+                    cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_MEDIUM_REMOVAL_PREVENTED);
+                    return;
+                }
+                SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT " eject", req->lun);
+                cd_scsi_lu_unload(dev);
+                cd_scsi_dev_changed(dev->tgt->user_data, req->lun);
+            }
+            dev->power_cond = CD_SCSI_POWER_STOPPED;
+            SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT " stopped", req->lun);
+        } else { /* start the unit */
+            dev->power_cond = CD_SCSI_POWER_ACTIVE;
+            SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT " started", req->lun);
+
+            if (load_eject) { /* load medium */
+                SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT " load with no media", req->lun);
+                cd_scsi_lu_load(dev, NULL);
+                cd_scsi_dev_changed(dev->tgt->user_data, req->lun);
+            }
+        }
+        break;
+    case CD_START_STOP_POWER_COND_ACTIVE:
+        /* not error to specify transition to the current power condition */
+        dev->power_cond = CD_SCSI_POWER_ACTIVE;
+        SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT " active", req->lun);
+        break;
+    case CD_START_STOP_POWER_COND_IDLE:
+    case CD_START_STOP_POWER_COND_FORCE_IDLE_0:
+        dev->power_cond = CD_SCSI_POWER_IDLE;
+        SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT " idle", req->lun);
+        break;
+    case CD_START_STOP_POWER_COND_STANDBY:
+    case CD_START_STOP_POWER_COND_FORCE_STANDBY_0:
+        dev->power_cond = CD_SCSI_POWER_STANDBY;
+        SPICE_DEBUG("start_stop_unit, lun:0x%" G_GUINT32_FORMAT " standby", req->lun);
+        break;
+    case CD_START_STOP_POWER_COND_LU_CONTROL:
+        break;
+    default:
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+#define CD_PERF_TYPE_PERFORMANCE                0x00
+#define CD_PERF_TYPE_UNUSABLE_AREA              0x01
+#define CD_PERF_TYPE_DEFECT_STATUS              0x02
+#define CD_PERF_TYPE_WRITE_SPEED                0x03
+
+#define CD_PERF_HEADER_LEN                      8
+
+#define CD_PERF_TYPE_PERFORMANCE_DESCR_LEN      16
+
+#define CD_PERF_TYPE_PERFORMANCE_REPORT_NOMINAL 0x00
+#define CD_PERF_TYPE_PERFORMANCE_REPORT_ALL     0x01
+#define CD_PERF_TYPE_PERFORMANCE_REPORT_EXCEPT  0x10
+
+
+static void cd_scsi_get_performance_resp_empty(CdScsiLU *dev, CdScsiRequest *req,
+                                               uint32_t type, uint32_t data_type,
+                                               uint32_t max_num_descr)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t write = (data_type >> 2) & 0x01;
+
+    memset(outbuf, 0, CD_PERF_HEADER_LEN);
+    if (write) {
+        outbuf[4] = 0x02;
+    }
+    req->in_len = CD_PERF_HEADER_LEN;
+
+    SPICE_DEBUG("get_performance, lun:%" G_GUINT32_FORMAT
+                " type:0x%x data_type:0x%x - sending empty response",
+                req->lun, type, data_type);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_get_performance_resp_performance(CdScsiLU *dev, CdScsiRequest *req,
+                                                     uint32_t start_lba,
+                                                     uint32_t data_type,
+                                                     uint32_t max_num_descr)
+{
+    uint8_t *outbuf = req->buf, *perf_desc;
+    uint32_t resp_len = CD_PERF_HEADER_LEN +
+                        CD_PERF_TYPE_PERFORMANCE_DESCR_LEN;
+    uint32_t perf_data_len = resp_len - 4; /* not incl. Perf Data Length */
+    uint32_t perf_kb = 10000;
+    uint32_t end_lba = dev->num_blocks - 1;
+    uint32_t except, write, tolerance;
+
+    except = data_type & 0x03;
+    if (except != CD_PERF_TYPE_PERFORMANCE_REPORT_ALL) {
+        start_lba = 0;
+    }
+    write = (data_type >> 2) & 0x01;
+    tolerance = (data_type >> 3) & 0x03;
+
+    SPICE_DEBUG("get_performance, lun:%" G_GUINT32_FORMAT
+                " performance type:0x00 data_type:0x%x"
+                " except:0x%x write:0x%x tolerance:0x%x"
+                " max_num:%" G_GUINT32_FORMAT,
+                req->lun, data_type, except, write,
+                tolerance, max_num_descr);
+
+    if (write) {
+        SPICE_DEBUG("get_performance, lun:%" G_GUINT32_FORMAT
+                " performance type:0x00 data_type:0x%x - write unsupported",
+                req->lun, data_type);
+        cd_scsi_get_performance_resp_empty(dev, req, CD_PERF_TYPE_PERFORMANCE,
+                                           data_type, max_num_descr);
+        return;
+    }
+
+    memset(outbuf, 0, resp_len);
+
+    outbuf[0] = (perf_data_len >> 24) & 0xff;
+    outbuf[1] = (perf_data_len >> 16) & 0xff;
+    outbuf[2] = (perf_data_len >> 8) & 0xff;
+    outbuf[3] = perf_data_len & 0xff;
+
+    perf_desc = outbuf + CD_PERF_HEADER_LEN;
+
+    perf_desc[0] = (start_lba >> 24) & 0xff;
+    perf_desc[1] = (start_lba >> 16) & 0xff;
+    perf_desc[2] = (start_lba >> 8) & 0xff;
+    perf_desc[3] = start_lba & 0xff;
+
+    perf_desc[4] = (perf_kb >> 24) & 0xff;
+    perf_desc[5] = (perf_kb >> 16) & 0xff;
+    perf_desc[6] = (perf_kb >> 8) & 0xff;
+    perf_desc[7] = perf_kb & 0xff;
+
+    perf_desc[8] = (end_lba >> 24) & 0xff;
+    perf_desc[9] = (end_lba >> 16) & 0xff;
+    perf_desc[10] = (end_lba >> 8) & 0xff;
+    perf_desc[11] = end_lba & 0xff;
+
+    perf_desc[12] = (perf_kb >> 24) & 0xff;
+    perf_desc[13] = (perf_kb >> 16) & 0xff;
+    perf_desc[14] = (perf_kb >> 8) & 0xff;
+    perf_desc[15] = perf_kb & 0xff;
+
+    req->req_len = CD_PERF_HEADER_LEN +
+                   (max_num_descr * CD_PERF_TYPE_PERFORMANCE_DESCR_LEN);
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_cmd_get_performance(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint32_t data_type, max_num_descr, start_lba, type;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    data_type = req->cdb[1] & 0x0f;
+    start_lba = (req->cdb[2] << 24) |
+                (req->cdb[3] << 16) |
+                (req->cdb[4] << 8) |
+                 req->cdb[5];
+    max_num_descr = (req->cdb[8] << 8) | req->cdb[9];
+    type = req->cdb[10];
+
+    switch (type) {
+    case CD_PERF_TYPE_PERFORMANCE:
+        cd_scsi_get_performance_resp_performance(dev, req, start_lba,
+                                                 data_type, max_num_descr);
+        break;
+    case CD_PERF_TYPE_UNUSABLE_AREA: /* not writable */
+    case CD_PERF_TYPE_DEFECT_STATUS: /* not restricted overwrite media */
+    case CD_PERF_TYPE_WRITE_SPEED: /* unsupported, irrelevant */
+    default:
+        SPICE_DEBUG("get_performance, lun:%" G_GUINT32_FORMAT
+                    " unsupported type:0x%x"
+                    " data_type:0x%x max_num:%" G_GUINT32_FORMAT,
+                    req->lun, type, data_type, max_num_descr);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        return;
+    }
+}
+
+#define CD_MECHANISM_STATUS_HDR_LEN     8
+
+/* byte 0 */
+#define CD_CHANGER_FAULT_FLAG           0x80
+
+#define CD_CHANGER_READY                0x00
+#define CD_CHANGER_LOADING              0x01
+#define CD_CHANGER_UNLOADING            0x02
+#define CD_CHANGER_INITIALIZING         0x03
+
+/* byte 1 */
+#define CD_CHANGER_DOOR_OPEN_FLAG       0x10
+
+#define CD_MECHANISM_STATE_IDLE         0x00
+#define CD_MECHANISM_STATE_PLAYING      0x01
+#define CD_MECHANISM_STATE_SCANNING     0x02
+/* ACTIVE: with Initiator, Composite or Other ports
+  (i.e. READ, PLAY CD, SCAN during PLAY CD) */
+#define CD_MECHANISM_STATE_ACTIVE       0x03
+#define CD_MECHANISM_STATE_NO_INFO      0x07
+
+/* slots */
+#define CD_MECHANISM_STATUS_SLOT_LEN    4
+
+#define CD_MECHANISM_SLOT_DISK_CHANGED  0x01
+#define CD_MECHANISM_SLOT_DISK_PRESENT  0x80
+#define CD_MECHANISM_SLOT_DISK_CWP_V    0x02
+#define CD_MECHANISM_SLOT_DISK_CWP      0x01
+
+static void cd_scsi_cmd_mechanism_status(CdScsiLU *dev, CdScsiRequest *req)
+{
+    uint8_t *outbuf = req->buf;
+    uint32_t resp_len = CD_MECHANISM_STATUS_HDR_LEN;
+
+    req->xfer_dir = SCSI_XFER_FROM_DEV;
+
+    req->req_len = (req->cdb[8] << 8) | req->cdb[9];
+    memset(outbuf, 0, req->req_len);
+
+    /* For non-changer devices the curent slot number field is reserved, set only status */
+    outbuf[0] |= (CD_CHANGER_READY << 5);
+
+    outbuf[1] |= (!dev->loaded) ? CD_CHANGER_DOOR_OPEN_FLAG : 0;
+    outbuf[1] |= (dev->power_cond == CD_POWER_STATUS_ACTIVE) ?
+                 (CD_MECHANISM_STATE_ACTIVE << 5) : (CD_MECHANISM_STATE_IDLE << 5);
+
+    /* For non-changer devices the number of slot tables returned shall be zero, so we leave
+       both 'Number of Slots Available' and 'Length of Slot Table' fields as zeros */
+
+    req->in_len = (req->req_len < resp_len) ? req->req_len : resp_len;
+
+    SPICE_DEBUG("mechanism_status, lun:%" G_GUINT32_FORMAT, req->lun);
+
+    cd_scsi_cmd_complete_good(dev, req);
+}
+
+static void cd_scsi_read_async_complete(GObject *src_object,
+                                        GAsyncResult *result,
+                                        gpointer user_data)
+{
+    GFileInputStream *stream = G_FILE_INPUT_STREAM(src_object);
+    CdScsiRequest *req = (CdScsiRequest *)user_data;
+    CdScsiTarget *st = (CdScsiTarget *)req->priv_data;
+    CdScsiLU *dev = &st->units[req->lun];
+    GError *error = NULL;
+    gsize bytes_read;
+    gboolean finished;
+
+    req->req_state = SCSI_REQ_COMPLETE;
+    req->cancel_id = 0;
+
+//    g_assert(stream == dev->stream);
+    if (stream != dev->stream) {
+        uint32_t opcode = (uint32_t)req->cdb[0];
+        SPICE_DEBUG("read_async_complete BAD STREAM, lun: %" G_GUINT32_FORMAT
+                    " req: %" G_GUINT64_FORMAT " op: 0x%02x",
+                    req->lun, req->req_len, opcode);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_TARGET_FAILURE);
+        cd_scsi_dev_request_complete(st->user_data, req);
+        return;
+    }
+
+    bytes_read = g_input_stream_read_finish(G_INPUT_STREAM(stream), result, &error);
+    finished = bytes_read > 0;
+    if (finished) {
+        SPICE_DEBUG("read_async_complete, lun: %" G_GUINT32_FORMAT
+                    " finished: %d bytes_read: %" G_GUINT64_FORMAT
+                    " req: %"  G_GUINT64_FORMAT,
+                    req->lun, finished, (uint64_t)bytes_read, req->req_len);
+
+        req->in_len = (bytes_read <= req->req_len) ? bytes_read : req->req_len;
+        req->status = GOOD;
+    } else {
+        if (error != NULL) {
+            SPICE_ERROR("g_input_stream_read_finish failed: %s", error->message);
+            g_clear_error (&error);
+        } else {
+            SPICE_ERROR("g_input_stream_read_finish failed (no err provided)");
+        }
+        req->in_len = 0;
+        req->status = GOOD;
+    }
+    cd_scsi_dev_request_complete(st->user_data, req);
+}
+
+static void cd_scsi_read_async_canceled(GCancellable *cancellable, gpointer user_data)
+{
+    CdScsiRequest *req = (CdScsiRequest *)user_data;
+    CdScsiTarget *st = (CdScsiTarget *)req->priv_data;
+
+    g_assert(cancellable == st->cancellable);
+    g_cancellable_disconnect(cancellable, req->cancel_id);
+    req->cancel_id = 0;
+
+    req->req_state = (st->state == CD_SCSI_TGT_STATE_RUNNING) ? SCSI_REQ_CANCELED : SCSI_REQ_DISPOSED;
+    req->in_len = 0;
+    req->status = GOOD;
+
+    cd_scsi_dev_request_complete(st->user_data, req);
+}
+
+static int cd_scsi_read_async_start(CdScsiLU *dev, CdScsiRequest *req)
+{
+    CdScsiTarget *st = dev->tgt;
+    GFileInputStream *stream = dev->stream;
+
+    SPICE_DEBUG("read_async_start, lun:%" G_GUINT32_FORMAT
+                " lba: %" G_GUINT64_FORMAT " offset: %" G_GUINT64_FORMAT
+                " cnt: %" G_GUINT64_FORMAT " len: %" G_GUINT64_FORMAT,
+                req->lun, req->lba, req->offset, req->count, req->req_len);
+
+    req->cancel_id = g_cancellable_connect(st->cancellable,
+                                           G_CALLBACK(cd_scsi_read_async_canceled),
+                                           req, /* data */
+                                           NULL); /* data destroy cb */
+    if (req->cancel_id == 0) {
+        /* already canceled */
+        return -1;
+    }
+
+    g_seekable_seek(G_SEEKABLE(stream),
+                    req->offset,
+                    G_SEEK_SET,
+                    NULL, /* cancellable */
+                    NULL); /* error */
+
+    g_input_stream_read_async(G_INPUT_STREAM(stream),
+                              req->buf, /* buffer to fill */
+                              req->req_len,
+                              G_PRIORITY_DEFAULT,
+                              st->cancellable,
+                              cd_scsi_read_async_complete,
+                              (gpointer)req); /* callback argument */
+    return 0;
+}
+
+static void cd_scsi_cmd_read(CdScsiLU *dev, CdScsiRequest *req)
+{
+    if (dev->power_cond == CD_SCSI_POWER_STOPPED) {
+        SPICE_DEBUG("read, lun: %" G_GUINT32_FORMAT " is stopped", req->lun);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INIT_CMD_REQUIRED);
+        return;
+    } else if (!dev->loaded || dev->stream == NULL) {
+        SPICE_DEBUG("read, lun: %" G_GUINT32_FORMAT " is not loaded", req->lun);
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_NOT_READY_NO_MEDIUM);
+        return;
+    }
+
+    req->cdb_len = scsi_cdb_length(req->cdb);
+
+    req->lba = scsi_cdb_lba(req->cdb, req->cdb_len);
+    req->offset = req->lba * dev->block_size;
+
+    req->count = scsi_cdb_xfer_length(req->cdb, req->cdb_len); /* xfer in blocks */
+    req->req_len = req->count * dev->block_size;
+
+    cd_scsi_read_async_start(dev, req);
+}
+
+void cd_scsi_dev_request_submit(void *scsi_target, CdScsiRequest *req)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+    uint32_t lun = req->lun;
+    uint32_t opcode = (uint32_t)req->cdb[0];
+    const char *cmd_name = scsi_cmd_name[opcode];
+    CdScsiLU *dev = &st->units[lun];
+
+    SPICE_DEBUG("request_submit, lun: %" G_GUINT32_FORMAT " op: 0x%02x %s", lun, opcode, cmd_name);
+
+    if (st->cur_req != NULL) {
+        SPICE_ERROR("request_submit, request not idle");
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_TARGET_FAILURE);
+        goto done;
+    }
+    if (req->req_state != SCSI_REQ_IDLE) {
+        SPICE_ERROR("request_submit, prev request outstanding");
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_TARGET_FAILURE);
+        goto done;
+    }
+    req->req_state = SCSI_REQ_RUNNING;
+    st->cur_req = req;
+
+    /* INQUIRY should send response even for non-existing LUNs */
+    if (!cd_scsi_target_lun_legal(st, lun)) {
+        SPICE_ERROR("request_submit, illegal lun:%" G_GUINT32_FORMAT, lun);
+        if (opcode == INQUIRY) {
+            if (req->cdb[1] & 0x1) {
+                cd_scsi_cmd_inquiry_vpd_no_lun(dev, req, PERIF_QUALIFIER_UNSUPPORTED);
+            } else {
+                cd_scsi_cmd_inquiry_standard_no_lun(dev, req, PERIF_QUALIFIER_UNSUPPORTED);
+            }
+        } else {
+            cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_LUN_NOT_SUPPORTED);
+        }
+        goto done;
+    }
+    if (!cd_scsi_target_lun_realized(st, lun)) {
+        SPICE_ERROR("request_submit, absent lun:%" G_GUINT32_FORMAT, lun);
+        if (opcode == INQUIRY) {
+            if (req->cdb[1] & 0x1) {
+                cd_scsi_cmd_inquiry_vpd_no_lun(dev, req, PERIF_QUALIFIER_NOT_CONNECTED);
+            } else {
+                cd_scsi_cmd_inquiry_standard_no_lun(dev, req, PERIF_QUALIFIER_NOT_CONNECTED);
+            }
+        } else {
+            cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_LUN_NOT_SUPPORTED);
+        }
+        goto done;
+    }
+
+    if (dev->short_sense.key != NO_SENSE) {
+        gboolean pending_sense = TRUE;
+        if (dev->short_sense.key == UNIT_ATTENTION) {
+            if (cd_scsi_opcode_ua_supress(opcode)) {
+                pending_sense = FALSE; /* UA supressed */
+            }
+        } else if (opcode == REQUEST_SENSE) {
+            pending_sense = FALSE; /* sense returned as data */
+        }
+        if (pending_sense) {
+            cd_scsi_cmd_complete_check_cond(dev, req, NULL); /* sense already set */
+            goto done;
+        }
+    }
+
+    /* save the target to be used in callbacks where only req is passed */
+    req->priv_data = (void *)st;
+
+    req->req_len = 0;
+
+    switch (opcode) {
+    case REPORT_LUNS:
+        cd_scsi_cmd_report_luns(st, dev, req);
+        break;
+    case TEST_UNIT_READY:
+        cd_scsi_cmd_test_unit_ready(dev, req);
+        break;
+    case INQUIRY:
+        cd_scsi_cmd_inquiry(dev, req);
+        break;
+    case REQUEST_SENSE:
+        cd_scsi_cmd_request_sense(dev, req);
+        break;
+    case READ_6:
+    case READ_10:
+    case READ_12:
+    case READ_16:
+        cd_scsi_cmd_read(dev, req);
+        break;
+    case READ_CAPACITY_10:
+        cd_scsi_cmd_read_capacity(dev, req);
+        break;
+    case READ_TOC:
+        cd_scsi_cmd_read_toc(dev, req);
+        break;
+    case GET_EVENT_STATUS_NOTIFICATION:
+        cd_scsi_cmd_get_event_status_notification(dev, req);
+        break;
+    case READ_DISC_INFORMATION:
+        cd_scsi_cmd_get_read_disc_information(dev, req);
+        break;
+    case READ_TRACK_INFORMATION:
+        cd_scsi_cmd_get_read_track_information(dev, req);
+        break;
+    case MODE_SENSE_10:
+        cd_scsi_cmd_mode_sense_10(dev, req);
+        break;
+    case MODE_SELECT:
+        cd_scsi_cmd_mode_select_6(dev, req);
+        break;
+    case MODE_SELECT_10:
+        cd_scsi_cmd_mode_select_10(dev, req);
+        break;
+    case GET_CONFIGURATION:
+        cd_scsi_cmd_get_configuration(dev, req);
+        break;
+    case ALLOW_MEDIUM_REMOVAL:
+        cd_scsi_cmd_allow_medium_removal(dev, req);
+        break;
+    case MMC_SEND_EVENT:
+        cd_scsi_cmd_send_event(dev, req);
+        break;
+    case MMC_REPORT_KEY:
+        cd_scsi_cmd_report_key(dev, req);
+        break;
+    case MMC_SEND_KEY:
+        cd_scsi_cmd_send_key(dev, req);
+        break;
+    case START_STOP:
+        cd_scsi_cmd_start_stop_unit(dev, req);
+        break;
+    case MMC_GET_PERFORMANCE:
+        cd_scsi_cmd_get_performance(dev, req);
+        break;
+    case MMC_MECHANISM_STATUS:
+        cd_scsi_cmd_mechanism_status(dev, req);
+        break;
+    default:
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_OPCODE);
+        break;
+    }
+
+    if (req->req_len > INT32_MAX) {
+        cd_scsi_cmd_complete_check_cond(dev, req, &sense_code_INVALID_CDB_FIELD);
+        goto done;
+    }
+
+done:
+    SPICE_DEBUG("request_submit done, lun: %" G_GUINT32_FORMAT
+                " op: 0x%02x %s, state: %s status: %" G_GUINT32_FORMAT " len: %" G_GUINT64_FORMAT,
+                lun, opcode, cmd_name, CdScsiReqState_str(req->req_state), req->status, req->in_len);
+
+    if (req->req_state == SCSI_REQ_COMPLETE) {
+        cd_scsi_dev_request_complete(st->user_data, req);
+    }
+}
+
+void cd_scsi_dev_request_cancel(void *scsi_target, CdScsiRequest *req)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+
+    if (st->cur_req == req) {
+        if (req->req_state == SCSI_REQ_RUNNING) {
+            SPICE_DEBUG("request_cancel: lun: %" G_GUINT32_FORMAT
+                         " op: 0x%02x len: %" G_GUINT64_FORMAT,
+                        req->lun, (unsigned int)req->cdb[0], req->req_len);
+            g_cancellable_cancel(st->cancellable);
+        } else {
+            SPICE_DEBUG("request_cancel: request is not running");
+        }
+    } else {
+        SPICE_DEBUG("request_cancel: other request is outstanding");
+    }
+}
+
+void cd_scsi_dev_request_release(void *scsi_target, CdScsiRequest *req)
+{
+    CdScsiTarget *st = (CdScsiTarget *)scsi_target;
+
+    st->cur_req = NULL;
+    cd_scsi_req_init(req);
+
+    if (st->state == CD_SCSI_TGT_STATE_RESET) {
+        cd_scsi_target_do_reset(st);
+    }
+}
+
+#endif /* USE_USBREDIR */
diff --git a/src/cd-scsi.h b/src/cd-scsi.h
new file mode 100644
index 0000000..b1d0ce2
--- /dev/null
+++ b/src/cd-scsi.h
@@ -0,0 +1,122 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   CD device emulation - SCSI engine
+
+   Copyright (C) 2018 Red Hat, Inc.
+
+   Red Hat Authors:
+   Alexander Nezhinsky<anezhins@redhat.com>
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   This library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef __CD_SCSI_H__
+#define __CD_SCSI_H__
+
+#include "cd-scsi-dev-params.h"
+#include "cd-usb-bulk-msd.h"
+#include "scsi-constants.h"
+
+#define FIXED_SENSE_CURRENT 0x70
+#define FIXED_SENSE_LEN 18
+
+#if defined(G_OS_WIN32)
+#include <winsock2.h>
+#include <windows.h>
+/* Windows is always LE at the moment */
+#define le32toh(x)          (x)
+#define htole32(x)          (x)
+#define htobe32(x)          htonl(x)
+#endif
+
+typedef enum _ScsiXferDir
+{
+    SCSI_XFER_NONE = 0,  /* TEST_UNIT_READY, ...           */
+    SCSI_XFER_FROM_DEV,  /* READ, INQUIRY, MODE_SENSE, ... */
+    SCSI_XFER_TO_DEV,    /* WRITE, MODE_SELECT, ...        */
+} ScsiXferDir;
+
+#define SCSI_CDB_BUF_SIZE   16
+#define SCSI_SENSE_BUF_SIZE 64
+
+typedef enum _CdScsiReqState
+{
+    SCSI_REQ_IDLE = 0,
+    SCSI_REQ_RUNNING,
+    SCSI_REQ_COMPLETE,
+    SCSI_REQ_CANCELED,
+    SCSI_REQ_DISPOSED,
+} CdScsiReqState;
+
+typedef struct _CdScsiRequest
+{
+    /* request */
+    uint8_t cdb[SCSI_CDB_BUF_SIZE];
+    uint32_t cdb_len;
+
+    uint32_t tag;
+    uint32_t lun;
+
+    uint8_t *buf;
+    uint32_t buf_len;
+
+    /* internal */
+    CdScsiReqState req_state;
+    ScsiXferDir xfer_dir;
+    uint64_t cancel_id;
+    void *priv_data;
+
+    uint64_t lba; /* offset in logical blocks if relevant */
+    uint64_t count; /* count in logical blocks */
+
+    uint64_t offset; /* scsi cdb offset, normalized to bytes */
+    uint64_t req_len; /* scsi cdb request length, normalized to bytes */
+
+    /* result */
+    uint64_t in_len; /* length of data actually available after read */
+    uint32_t status; /* SCSI status code */
+
+} CdScsiRequest;
+
+CdScsiReqState cd_scsi_get_req_state(CdScsiRequest *req);
+
+/* SCSI target/device API */
+
+void *cd_scsi_target_alloc(void *target_user_data, uint32_t max_luns); /* to be used in callbacks */
+void cd_scsi_target_free(void *scsi_target);
+
+int cd_scsi_dev_realize(void *scsi_target, uint32_t lun, const CdScsiDeviceParameters *dev_params);
+int cd_scsi_dev_unrealize(void *scsi_target, uint32_t lun);
+
+int cd_scsi_dev_lock(void *scsi_target, uint32_t lun, gboolean lock);
+int cd_scsi_dev_load(void *scsi_target, uint32_t lun, const CdScsiMediaParameters *media_params);
+int cd_scsi_dev_get_info(void *scsi_target, uint32_t lun, CdScsiDeviceInfo *lun_info);
+int cd_scsi_dev_unload(void *scsi_target, uint32_t lun);
+
+void cd_scsi_dev_request_submit(void *scsi_target, CdScsiRequest *request);
+void cd_scsi_dev_request_cancel(void *scsi_target, CdScsiRequest *request);
+void cd_scsi_dev_request_release(void *scsi_target, CdScsiRequest *request);
+
+int cd_scsi_dev_reset(void *scsi_target, uint32_t lun);
+
+int cd_scsi_target_reset(void *scsi_target);
+
+/* Callbacks */
+
+void cd_scsi_dev_request_complete(void *target_user_data, CdScsiRequest *request);
+void cd_scsi_dev_changed(void *target_user_data, uint32_t lun);
+void cd_scsi_dev_reset_complete(void *target_user_data, uint32_t lun);
+void cd_scsi_target_reset_complete(void *target_user_data);
+
+#endif /* __CD_SCSI_H__ */
diff --git a/src/cd-usb-bulk-msd.c b/src/cd-usb-bulk-msd.c
new file mode 100644
index 0000000..be0fee3
--- /dev/null
+++ b/src/cd-usb-bulk-msd.c
@@ -0,0 +1,555 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+    USB CD Device emulation - Data Bulk transfers - Mass Storage Device
+
+    Copyright (C) 2018 Red Hat, Inc.
+
+    Red Hat Authors:
+    Alexander Nezhinsky<anezhins@redhat.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library 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
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "config.h"
+#include "spice/types.h"
+#include "spice-common.h"
+#include "spice-util.h"
+#include "cd-usb-bulk-msd.h"
+#include "cd-scsi.h"
+
+#ifdef USE_USBREDIR
+
+#define SPICE_ERROR(fmt, ...) \
+    do { SPICE_DEBUG("usb-msd error: " fmt , ## __VA_ARGS__); } while (0)
+
+typedef enum _UsbCdState {
+    USB_CD_STATE_INIT, /* Not ready */
+    USB_CD_STATE_CBW, /* Waiting for Command Block */
+    USB_CD_STATE_DATAOUT, /* Transfer data to device */
+    USB_CD_STATE_DATAIN, /* Transfer data from device */
+    USB_CD_STATE_ZERO_DATAIN, /* Need to send zero bulk-in before status */
+    USB_CD_STATE_CSW, /* Send Command Status */
+    USB_CD_STATE_DEVICE_RESET, /* reset of a single device */
+    USB_CD_STATE_TARGET_RESET /* reset of entire target */
+} UsbCdState;
+
+struct __attribute__((__packed__)) UsbCdCBW {
+    uint32_t sig;
+    uint32_t tag;
+    uint32_t exp_data_len; /* expected data xfer length for the request */
+    uint8_t flags;
+    uint8_t lun;
+    uint8_t cmd_len; /* actual length of the scsi command that follows */
+    uint8_t cmd[16]; /* scsi command to perform */
+};
+
+struct __attribute__((__packed__)) UsbCdCSW {
+    uint32_t sig;
+    uint32_t tag;
+    uint32_t residue;
+    uint8_t status;
+};
+
+typedef enum _UsbMsdStatus {
+    USB_MSD_STATUS_GOOD = 0,
+    USB_MSD_STATUS_FAILED = 1,
+    USB_MSD_STATUS_PHASE_ERR = 2,
+} UsbMsdStatus;
+
+typedef struct _UsbCdBulkMsdRequest
+{
+    CdScsiRequest scsi_req;
+
+    uint32_t lun;
+    uint32_t usb_tag;
+    uint32_t usb_req_len; /* length of data requested by usb */
+    uint32_t scsi_in_len; /* length of data returned by scsi limited by usb request */
+
+    uint32_t xfer_len; /* length of data transfered until now */
+    uint32_t bulk_in_len; /* length of the last postponed bulk-in request */
+
+    struct UsbCdCSW csw; /* usb status header */
+} UsbCdBulkMsdRequest;
+
+typedef struct _UsbCdBulkMsdDevice
+{
+    UsbCdState state;
+    void *scsi_target; /* scsi handle */
+    void *usb_user_data; /* used in callbacks to usb */
+    UsbCdBulkMsdRequest usb_req; /* now supporting a single cmd */
+    uint8_t *data_buf;
+    uint32_t data_buf_len;
+} UsbCdBulkMsdDevice;
+
+static inline const char *usb_cd_state_str(UsbCdState state)
+{
+    switch(state)
+    {
+    case USB_CD_STATE_INIT:
+        return "INIT";
+    case USB_CD_STATE_CBW:
+        return "CBW";
+    case USB_CD_STATE_DATAOUT:
+        return "DATAOUT";
+    case USB_CD_STATE_DATAIN:
+        return "DATAIN";
+    case USB_CD_STATE_ZERO_DATAIN:
+        return "ZERO_DATAIN";
+    case USB_CD_STATE_CSW:
+        return "CSW";
+    case USB_CD_STATE_DEVICE_RESET:
+        return "DEV_RESET";
+    case USB_CD_STATE_TARGET_RESET:
+        return "TGT_RESET";
+    default:
+        return "ILLEGAL";
+    }
+}
+
+static void cd_usb_bulk_msd_set_state(UsbCdBulkMsdDevice *cd, UsbCdState state)
+{
+    SPICE_DEBUG("State %s -> %s", usb_cd_state_str(cd->state), usb_cd_state_str(state));
+    cd->state = state;
+}
+
+void *cd_usb_bulk_msd_alloc(void *usb_user_data, uint32_t max_luns)
+{
+    UsbCdBulkMsdDevice *cd;
+
+    cd = g_malloc0(sizeof(*cd));
+
+    cd->data_buf_len = 256 * 1024;
+    cd->data_buf = g_malloc(cd->data_buf_len);
+
+    cd->scsi_target = cd_scsi_target_alloc(cd, max_luns);
+    if (cd->scsi_target == NULL) {
+        g_free(cd);
+        return NULL;
+    }
+    cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_INIT);
+    cd->usb_user_data = usb_user_data;
+
+    SPICE_DEBUG("Alloc, max_luns:%" G_GUINT32_FORMAT, max_luns);
+    return cd;
+}
+
+int cd_usb_bulk_msd_realize(void *device, uint32_t lun,
+                            const CdScsiDeviceParameters *dev_params)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    CdScsiDeviceParameters scsi_dev_params;
+    int rc;
+
+    scsi_dev_params.vendor = dev_params->vendor ? : "SPICE";
+    scsi_dev_params.product = dev_params->product ? : "USB-CD";
+    scsi_dev_params.version = dev_params->version ? : "0.1";
+    scsi_dev_params.serial = dev_params->serial ? : "123456";
+
+    rc = cd_scsi_dev_realize(cd->scsi_target, lun, &scsi_dev_params);
+    if (rc != 0) {
+        SPICE_ERROR("Failed to realize lun:%" G_GUINT32_FORMAT, lun);
+        return rc;
+    }
+
+    if (cd->state == USB_CD_STATE_INIT) {
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CBW);
+        cd_scsi_dev_request_release(cd->scsi_target, &cd->usb_req.scsi_req);
+    }
+
+    SPICE_DEBUG("Realize OK lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+int cd_usb_bulk_msd_lock(void *device, uint32_t lun, gboolean lock)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    int rc;
+
+    rc = cd_scsi_dev_lock(cd->scsi_target, lun, lock);
+    if (rc != 0) {
+        SPICE_ERROR("Failed to lock lun:%" G_GUINT32_FORMAT, lun);
+        return rc;
+    }
+
+    SPICE_DEBUG("Lock OK lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+int cd_usb_bulk_msd_load(void *device, uint32_t lun,
+                         const CdScsiMediaParameters *media_params)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    int rc;
+
+    rc = cd_scsi_dev_load(cd->scsi_target, lun, media_params);
+    if (rc != 0) {
+        SPICE_ERROR("Failed to load lun:%" G_GUINT32_FORMAT, lun);
+        return rc;
+    }
+
+    SPICE_DEBUG("Load OK lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+int cd_usb_bulk_msd_get_info(void *device, uint32_t lun, CdScsiDeviceInfo *lun_info)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    int rc;
+
+    rc = cd_scsi_dev_get_info(cd->scsi_target, lun, lun_info);
+    if (rc != 0) {
+        SPICE_ERROR("Failed to get info lun:%" G_GUINT32_FORMAT, lun);
+        return rc;
+    }
+
+    return 0;
+}
+
+int cd_usb_bulk_msd_unload(void *device, uint32_t lun)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    int rc;
+
+    rc = cd_scsi_dev_unload(cd->scsi_target, lun);
+    if (rc != 0) {
+        SPICE_ERROR("Failed to unload lun:%" G_GUINT32_FORMAT, lun);
+        return rc;
+    }
+
+    SPICE_DEBUG("Unload OK lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+int cd_usb_bulk_msd_unrealize(void *device, uint32_t lun)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    int rc;
+
+    rc = cd_scsi_dev_unrealize(cd->scsi_target, lun);
+    if (rc != 0) {
+        SPICE_ERROR("Unrealize lun:%" G_GUINT32_FORMAT, lun);
+        return rc;
+    }
+
+    SPICE_DEBUG("Unrealize lun:%" G_GUINT32_FORMAT, lun);
+    return 0;
+}
+
+void cd_usb_bulk_msd_free(void *device)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+
+    cd_scsi_target_free(cd->scsi_target);
+    g_free(cd);
+
+    SPICE_DEBUG("Free");
+}
+
+int cd_usb_bulk_msd_reset(void *device)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+
+    cd_scsi_target_reset(cd->scsi_target);
+    cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CBW);
+
+    SPICE_DEBUG("Reset");
+    return 0;
+}
+
+static int parse_usb_msd_cmd(UsbCdBulkMsdDevice *cd, uint8_t *buf, uint32_t cbw_len)
+{
+    struct UsbCdCBW *cbw = (struct UsbCdCBW *)buf;
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+    CdScsiRequest *scsi_req = &usb_req->scsi_req;
+
+    if (cbw_len != 31) {
+        SPICE_ERROR("CMD: Bad CBW size:%" G_GUINT32_FORMAT, cbw_len);
+        return -1;
+    }
+    if (le32toh(cbw->sig) != 0x43425355) { /* MSD command signature */
+        SPICE_ERROR("CMD: Bad CBW signature:%08x", le32toh(cbw->sig));
+        return -1;
+    }
+
+    usb_req->lun = cbw->lun;
+    usb_req->usb_tag = le32toh(cbw->tag);
+    usb_req->usb_req_len = le32toh(cbw->exp_data_len);
+
+    usb_req->scsi_in_len = 0; /* no data from scsi yet */
+    usb_req->xfer_len = 0; /* no bulks transfered yet */
+    usb_req->bulk_in_len = 0; /* no bulk-in requests yet */
+
+    if (usb_req->usb_req_len == 0) {
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CSW); /* no data - return status */
+        scsi_req->buf = NULL;
+        scsi_req->buf_len = 0;
+    } else if (cbw->flags & 0x80) {
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_DATAIN); /* read command */
+        scsi_req->buf = cd->data_buf;
+        scsi_req->buf_len = cd->data_buf_len;
+    } else {
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_DATAOUT); /* write command */
+        scsi_req->buf = NULL;
+        scsi_req->buf_len = 0;
+    }
+
+    scsi_req->cdb_len = ((uint32_t)cbw->cmd_len) & 0x1F;
+    g_assert(scsi_req->cdb_len <= sizeof(scsi_req->cdb));
+    memcpy(scsi_req->cdb, cbw->cmd, scsi_req->cdb_len);
+
+    scsi_req->tag = usb_req->usb_tag;
+    scsi_req->lun = usb_req->lun;
+
+    SPICE_DEBUG("CMD lun:%" G_GUINT32_FORMAT " tag:0x%x flags:%08x "
+        "cdb_len:%" G_GUINT32_FORMAT " req_len:%" G_GUINT32_FORMAT,
+        usb_req->lun, usb_req->usb_tag, cbw->flags,
+        scsi_req->cdb_len, usb_req->usb_req_len);
+
+    /* prepare status - CSW */
+    usb_req->csw.sig = htole32(0x53425355);
+    usb_req->csw.tag = cbw->tag;
+    usb_req->csw.residue = 0;
+    usb_req->csw.status = (uint8_t)USB_MSD_STATUS_GOOD;
+
+    return 0;
+}
+
+static void usb_cd_cmd_done(UsbCdBulkMsdDevice *cd)
+{
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+    CdScsiRequest *scsi_req = &usb_req->scsi_req;
+
+    cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CBW); /* Command next */
+    cd_scsi_dev_request_release(cd->scsi_target, scsi_req);
+}
+
+static void usb_cd_send_status(UsbCdBulkMsdDevice *cd)
+{
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+
+    SPICE_DEBUG("Command CSW tag:0x%x msd_status:%d len:%" G_GUINT64_FORMAT,
+                le32toh(usb_req->csw.tag), (int)usb_req->csw.status, sizeof(usb_req->csw));
+
+    usb_cd_cmd_done(cd);
+
+    g_assert(usb_req->csw.sig == htole32(0x53425355));
+    cd_usb_bulk_msd_read_complete(cd->usb_user_data,
+                                  (uint8_t *)&usb_req->csw, sizeof(usb_req->csw),
+                                  BULK_STATUS_GOOD);
+}
+
+static void usb_cd_send_canceled(UsbCdBulkMsdDevice *cd)
+{
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+
+    SPICE_DEBUG("Canceled cmd tag:0x%x, len:%" G_GUINT64_FORMAT,
+                le32toh(usb_req->csw.tag), sizeof(usb_req->csw));
+
+    usb_cd_cmd_done(cd);
+
+    cd_usb_bulk_msd_read_complete(cd->usb_user_data,
+                                  NULL, 0,
+                                  BULK_STATUS_CANCELED);
+}
+
+static void usb_cd_send_data_in(UsbCdBulkMsdDevice *cd, uint32_t max_len)
+{
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+    CdScsiRequest *scsi_req = &usb_req->scsi_req;
+    uint8_t *buf = ((uint8_t *)scsi_req->buf) + usb_req->xfer_len;
+    uint32_t avail_len = usb_req->scsi_in_len - usb_req->xfer_len;
+    uint32_t send_len = (avail_len <= max_len) ? avail_len : max_len;
+
+    SPICE_DEBUG("Data-in cmd tag 0x%x, remains %" G_GUINT32_FORMAT
+                ", requested %" G_GUINT32_FORMAT ", send %" G_GUINT32_FORMAT,
+                usb_req->csw.tag, avail_len, max_len, send_len);
+
+    g_assert(max_len <= usb_req->usb_req_len);
+
+    cd_usb_bulk_msd_read_complete(cd->usb_user_data,
+                                  buf, send_len,
+                                  BULK_STATUS_GOOD);
+
+    if (scsi_req->status == GOOD) {
+        usb_req->xfer_len += send_len;
+        if (usb_req->xfer_len == usb_req->scsi_in_len) {
+            /* all data for this bulk has been transferred */
+            if (usb_req->scsi_in_len == usb_req->usb_req_len || /* req fully satisfiled */
+                send_len < max_len) { /* partial bulk - no more data */
+                cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CSW);
+            } else {
+                /* partial cmd data fullfilled entire vulk-in request */
+                cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_ZERO_DATAIN);
+            }
+        }
+    } else { /* cmd failed - no more data */
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CSW);
+    }
+}
+
+int cd_usb_bulk_msd_read(void *device, uint32_t max_len)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+    CdScsiRequest *scsi_req = &usb_req->scsi_req;
+
+    SPICE_DEBUG("msd_read, state: %s, len %" G_GUINT32_FORMAT,
+                usb_cd_state_str(cd->state), max_len);
+
+    switch (cd->state) {
+    case USB_CD_STATE_CSW: /* Command Status */
+        if (max_len < 13) {
+            goto fail;
+        }
+        if (cd_scsi_get_req_state(scsi_req) == SCSI_REQ_COMPLETE) {
+            usb_cd_send_status(cd);
+        } else {
+            usb_req->bulk_in_len += max_len;
+            SPICE_DEBUG("msd_read CSW, req incomplete, added len %" G_GUINT32_FORMAT
+                        " saved len %" G_GUINT32_FORMAT,
+                        max_len, usb_req->bulk_in_len);
+        }
+        break;
+
+    case USB_CD_STATE_DATAIN: /* Bulk Data-IN */
+        if (cd_scsi_get_req_state(scsi_req) == SCSI_REQ_COMPLETE) {
+            usb_cd_send_data_in(cd, max_len);
+        } else {
+            usb_req->bulk_in_len += max_len;
+            SPICE_DEBUG("msd_read DATAIN, req incomplete, added len %" G_GUINT32_FORMAT
+                        " saved len %" G_GUINT32_FORMAT,
+                        max_len, usb_req->bulk_in_len);
+        }
+        break;
+
+    case USB_CD_STATE_ZERO_DATAIN:
+        cd_usb_bulk_msd_read_complete(cd->usb_user_data,
+                                      NULL, 0,
+                                      BULK_STATUS_GOOD);
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CSW); /* Status next */
+        break;
+
+    default:
+        SPICE_ERROR("Unexpected read state: %s, len %" G_GUINT32_FORMAT,
+                    usb_cd_state_str(cd->state), max_len);
+        goto fail;
+    }
+    return 0;
+
+fail:
+    return -1;
+}
+
+void cd_scsi_dev_request_complete(void *target_user_data, CdScsiRequest *scsi_req)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)target_user_data;
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+
+    g_assert(scsi_req == &usb_req->scsi_req);
+
+    if (scsi_req->req_state == SCSI_REQ_COMPLETE) {
+
+        usb_req->scsi_in_len = (scsi_req->in_len <= usb_req->usb_req_len) ?
+                                scsi_req->in_len : usb_req->usb_req_len;
+
+        /* prepare CSW */
+        if (usb_req->usb_req_len > usb_req->scsi_in_len) {
+            usb_req->csw.residue = htole32(usb_req->usb_req_len - usb_req->scsi_in_len);
+        }
+        if (scsi_req->status != GOOD) {
+            usb_req->csw.status = (uint8_t)USB_MSD_STATUS_FAILED;
+        }
+
+        if (usb_req->bulk_in_len) {
+            /* bulk-in request arrived while scsi was still running */
+            if (cd->state == USB_CD_STATE_DATAIN) {
+                usb_cd_send_data_in(cd, usb_req->bulk_in_len);
+            } else if (cd->state == USB_CD_STATE_CSW) {
+                usb_cd_send_status(cd);
+            }
+            usb_req->bulk_in_len = 0;
+        }
+    } else if (scsi_req->req_state == SCSI_REQ_CANCELED) {
+        usb_cd_send_canceled(cd);
+    } else {
+        g_assert(scsi_req->req_state == SCSI_REQ_DISPOSED);
+        SPICE_DEBUG("Disposed cmd tag:0x%x, len:%" G_GUINT64_FORMAT,
+                le32toh(usb_req->csw.tag), sizeof(usb_req->csw));
+        usb_cd_cmd_done(cd);
+    }
+}
+
+int cd_usb_bulk_msd_cancel_read(void *device)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+    UsbCdBulkMsdRequest *usb_req = &cd->usb_req;
+    CdScsiRequest *scsi_req = &usb_req->scsi_req;
+
+    cd_scsi_dev_request_cancel(cd->scsi_target, scsi_req);
+    return 0;
+}
+
+int cd_usb_bulk_msd_write(void *device, uint8_t *buf_out, uint32_t buf_out_len)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)device;
+
+    switch (cd->state) {
+    case USB_CD_STATE_CBW: /* Command Block */
+        parse_usb_msd_cmd(cd, buf_out, buf_out_len);
+        if (cd->state == USB_CD_STATE_DATAIN || cd->state == USB_CD_STATE_CSW) {
+            cd_scsi_dev_request_submit(cd->scsi_target, &cd->usb_req.scsi_req);
+        }
+        break;
+    case USB_CD_STATE_DATAOUT: /* Data-Out for a Write cmd */
+        cd->usb_req.scsi_req.buf = buf_out;
+        cd->usb_req.scsi_req.buf_len = buf_out_len;
+        cd_scsi_dev_request_submit(cd->scsi_target, &cd->usb_req.scsi_req);
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CSW); /* Status next */
+        break;
+    default:
+        SPICE_DEBUG("Unexpected write state: %s, len %" G_GUINT32_FORMAT,
+                    usb_cd_state_str(cd->state), buf_out_len);
+        goto fail;
+    }
+    return 0;
+
+fail:
+    return -1;
+}
+
+void cd_scsi_dev_reset_complete(void *target_user_data, uint32_t lun)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)target_user_data;
+
+    if (cd->state == USB_CD_STATE_DEVICE_RESET) {
+        cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_CBW);
+        cd_usb_bulk_msd_reset_complete(cd->usb_user_data, 0);
+    }
+}
+
+void cd_scsi_target_reset_complete(void *target_user_data)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)target_user_data;
+    cd_usb_bulk_msd_set_state(cd, USB_CD_STATE_INIT);
+}
+
+void cd_scsi_dev_changed(void *target_user_data, uint32_t lun)
+{
+    UsbCdBulkMsdDevice *cd = (UsbCdBulkMsdDevice *)target_user_data;
+    SPICE_DEBUG("Device changed, state: %s lun: %" G_GUINT32_FORMAT,
+                usb_cd_state_str(cd->state), lun);
+    cd_usb_bulk_msd_lun_changed(cd->usb_user_data, lun);
+}
+
+#endif /* USE_USBREDIR */
diff --git a/src/cd-usb-bulk-msd.h b/src/cd-usb-bulk-msd.h
new file mode 100644
index 0000000..1098a87
--- /dev/null
+++ b/src/cd-usb-bulk-msd.h
@@ -0,0 +1,134 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+    Copyright (C) 2018 Red Hat, Inc.
+
+    Red Hat Authors:
+    Alexander Nezhinsky<anezhins@redhat.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library 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
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef __CD_USB_BULK_MSD_H__
+#define __CD_USB_BULK_MSD_H__
+
+G_BEGIN_DECLS
+
+#include <gio/gio.h>
+
+#include "cd-scsi-dev-params.h"
+
+typedef enum _CdUsbBulkStatus
+{
+    BULK_STATUS_GOOD = 0,
+    BULK_STATUS_ERROR,
+    BULK_STATUS_CANCELED,
+    BULK_STATUS_STALL,
+} CdUsbBulkStatus;
+
+/* USB backend callbacks */
+
+/* called on completed read data bulk transfer
+   user_data - user_data in unit parameters structure
+   status - bulk status code
+*/
+void cd_usb_bulk_msd_read_complete(void *user_data,
+                                   uint8_t *data, uint32_t length,
+                                   CdUsbBulkStatus status);
+
+/* called when state of device's unit changed to signal GUI component
+   user_data - user_data in unit parameters structure
+*/
+void cd_usb_bulk_msd_lun_changed(void *user_data, uint32_t lun);
+
+/* called on completed device reset
+   user_data - user_data in unit parameters structure
+   status - error code
+*/
+void cd_usb_bulk_msd_reset_complete(void *user_data,
+                                    int status);
+
+/* MSD backend api */
+
+/* allocate new device descriptor */
+void *cd_usb_bulk_msd_alloc(void *user_data, uint32_t max_lun);
+
+/* free device descriptor */
+void cd_usb_bulk_msd_free(void *device);
+
+/* configure a new Logical Unit to be represented by the device
+   returns: error code
+*/
+int cd_usb_bulk_msd_realize(void *device, uint32_t lun,
+                            const CdScsiDeviceParameters *dev_params);
+
+/* lock the device, prevent unloading
+   returns: error code
+*/
+int cd_usb_bulk_msd_lock(void *device, uint32_t lun, gboolean lock);
+
+/* load new media, if already loaded, simulate media change
+   returns: error code
+*/
+int cd_usb_bulk_msd_load(void *device, uint32_t lun,
+                         const CdScsiMediaParameters *media_params);
+
+/* query unit parameters and status
+   returns: error code
+*/
+int cd_usb_bulk_msd_get_info(void *device, uint32_t lun,
+                             CdScsiDeviceInfo *lun_info);
+
+/* unload the media
+   returns: error code
+*/
+int cd_usb_bulk_msd_unload(void *device, uint32_t lun);
+
+/* detach a Logical Unit
+   returns: error code
+*/
+int cd_usb_bulk_msd_unrealize(void *device, uint32_t lun);
+
+/* reset the device instance; cancel all IO ops, reset state
+   returns: error code
+*/
+int cd_usb_bulk_msd_reset(void *device);
+
+
+/* perform a write data bulk transfer
+   data_len - length of available data to write
+   returns: error code
+*/
+int cd_usb_bulk_msd_write(void *device,
+                          uint8_t *buf, uint32_t data_len);
+
+
+/* perform a read data bulk transfer
+   max_len - length of available buffer to fill
+   If data available immediately, should call cd_usb_bulk_msd_read_complete()
+     and return success
+   If fatal error detected immediately, should call cd_usb_bulk_msd_read_complete()
+     with error code and return success
+
+   returns: 0 - success, -1 - error
+*/
+int cd_usb_bulk_msd_read(void *device, uint32_t max_len);
+
+/* cancels pending read data bulk transfer
+   returns: error code
+*/
+int cd_usb_bulk_msd_cancel_read(void *device);
+
+G_END_DECLS
+
+#endif /* __CD_USB_BULK_MSD_H__ */
diff --git a/src/scsi-constants.h b/src/scsi-constants.h
new file mode 100644
index 0000000..31da750
--- /dev/null
+++ b/src/scsi-constants.h
@@ -0,0 +1,324 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   Copyright (C) 2018 Red Hat, Inc.
+   Based on the GLib header
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   This library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef __SCSI_CONSTANTS_H__
+#define __SCSI_CONSTANTS_H__
+
+/*
+ *      SCSI opcodes
+ */
+
+#define TEST_UNIT_READY       0x00
+#define REWIND                0x01
+#define REQUEST_SENSE         0x03
+#define FORMAT_UNIT           0x04
+#define READ_BLOCK_LIMITS     0x05
+#define INITIALIZE_ELEMENT_STATUS 0x07
+#define REASSIGN_BLOCKS_      0x07
+#define READ_6                0x08
+#define WRITE_6               0x0a
+#define SET_CAPACITY          0x0b
+#define READ_REVERSE          0x0f
+#define WRITE_FILEMARKS       0x10
+#define SPACE                 0x11
+#define INQUIRY               0x12
+#define RECOVER_BUFFERED_DATA 0x14
+#define MODE_SELECT           0x15
+#define RESERVE               0x16
+#define RELEASE               0x17
+#define COPY                  0x18
+#define ERASE                 0x19
+#define MODE_SENSE            0x1a
+#define LOAD_UNLOAD           0x1b
+#define SCAN                  0x1b
+#define START_STOP            0x1b
+#define RECEIVE_DIAGNOSTIC    0x1c
+#define SEND_DIAGNOSTIC       0x1d
+#define ALLOW_MEDIUM_REMOVAL  0x1e
+#define SET_WINDOW            0x24
+#define READ_CAPACITY_10      0x25
+#define GET_WINDOW            0x25
+#define READ_10               0x28
+#define WRITE_10              0x2a
+#define SEND                  0x2a
+#define SEEK_10               0x2b
+#define LOCATE_10             0x2b
+#define POSITION_TO_ELEMENT   0x2b
+#define WRITE_VERIFY_10       0x2e
+#define VERIFY_10             0x2f
+#define SEARCH_HIGH           0x30
+#define SEARCH_EQUAL          0x31
+#define OBJECT_POSITION       0x31
+#define SEARCH_LOW            0x32
+#define SET_LIMITS            0x33
+#define PRE_FETCH             0x34
+#define READ_POSITION         0x34
+#define GET_DATA_BUFFER_STATUS 0x34
+#define SYNCHRONIZE_CACHE     0x35
+#define LOCK_UNLOCK_CACHE     0x36
+#define INITIALIZE_ELEMENT_STATUS_WITH_RANGE 0x37
+#define READ_DEFECT_DATA      0x37
+#define MEDIUM_SCAN           0x38
+#define COMPARE               0x39
+#define COPY_VERIFY           0x3a
+#define WRITE_BUFFER          0x3b
+#define READ_BUFFER           0x3c
+#define UPDATE_BLOCK          0x3d
+#define READ_LONG_10          0x3e
+#define WRITE_LONG_10         0x3f
+#define CHANGE_DEFINITION     0x40
+#define WRITE_SAME_10         0x41
+#define UNMAP                 0x42
+#define READ_TOC              0x43
+#define REPORT_DENSITY_SUPPORT 0x44
+#define GET_CONFIGURATION     0x46
+#define SANITIZE              0x48
+#define GET_EVENT_STATUS_NOTIFICATION 0x4a
+#define LOG_SELECT            0x4c
+#define LOG_SENSE             0x4d
+#define READ_DISC_INFORMATION 0x51
+#define READ_TRACK_INFORMATION 0x52
+#define RESERVE_TRACK         0x53
+#define MODE_SELECT_10        0x55
+#define RESERVE_10            0x56
+#define RELEASE_10            0x57
+#define MODE_SENSE_10         0x5a
+#define SEND_CUE_SHEET        0x5d
+#define PERSISTENT_RESERVE_IN 0x5e
+#define PERSISTENT_RESERVE_OUT 0x5f
+#define VARLENGTH_CDB         0x7f
+#define WRITE_FILEMARKS_16    0x80
+#define READ_REVERSE_16       0x81
+#define ALLOW_OVERWRITE       0x82
+#define EXTENDED_COPY         0x83
+#define ATA_PASSTHROUGH_16    0x85
+#define ACCESS_CONTROL_IN     0x86
+#define ACCESS_CONTROL_OUT    0x87
+#define READ_16               0x88
+#define COMPARE_AND_WRITE     0x89
+#define WRITE_16              0x8a
+#define WRITE_VERIFY_16       0x8e
+#define VERIFY_16             0x8f
+#define PRE_FETCH_16          0x90
+#define SPACE_16              0x91
+#define SYNCHRONIZE_CACHE_16  0x91
+#define LOCATE_16             0x92
+#define WRITE_SAME_16         0x93
+#define ERASE_16              0x93
+#define SERVICE_ACTION_IN_16  0x9e
+#define WRITE_LONG_16         0x9f
+#define REPORT_LUNS           0xa0
+#define ATA_PASSTHROUGH_12    0xa1
+#define MAINTENANCE_IN        0xa3
+#define MAINTENANCE_OUT       0xa4
+#define MOVE_MEDIUM           0xa5
+#define EXCHANGE_MEDIUM       0xa6
+#define SET_READ_AHEAD        0xa7
+#define READ_12               0xa8
+#define WRITE_12              0xaa
+#define SERVICE_ACTION_IN_12  0xab
+#define ERASE_12              0xac
+#define WRITE_VERIFY_12       0xae
+#define VERIFY_12             0xaf
+#define SEARCH_HIGH_12        0xb0
+#define SEARCH_EQUAL_12       0xb1
+#define SEARCH_LOW_12         0xb2
+#define READ_ELEMENT_STATUS   0xb8
+#define SEND_VOLUME_TAG       0xb6
+
+/* MMC-specific opcode assignment */
+#define MMC_SEND_EVENT          0xa2
+#define MMC_SEND_KEY            0xa3
+#define MMC_REPORT_KEY          0xa4
+#define MMC_GET_PERFORMANCE     0xac
+#define MMC_READ_DVD_STRUCTURE  0xad
+#define MMC_READ_DEFECT_DATA_12 0xb7
+#define MMC_SET_CD_SPEED        0xbb
+#define MMC_MECHANISM_STATUS    0xbd
+#define MMC_READ_CD             0xbe
+#define MMC_SEND_DVD_STRUCTURE  0xbf
+
+/*
+ * SERVICE ACTION IN subcodes
+ */
+#define SAI_READ_CAPACITY_16  0x10
+
+/*
+ * READ POSITION service action codes
+ */
+#define SHORT_FORM_BLOCK_ID  0x00
+#define SHORT_FORM_VENDOR_SPECIFIC 0x01
+#define LONG_FORM            0x06
+#define EXTENDED_FORM        0x08
+
+/*
+ *  SAM Status codes
+ */
+
+#define GOOD                 0x00
+#define CHECK_CONDITION      0x02
+#define CONDITION_GOOD       0x04
+#define BUSY                 0x08
+#define INTERMEDIATE_GOOD    0x10
+#define INTERMEDIATE_C_GOOD  0x14
+#define RESERVATION_CONFLICT 0x18
+#define COMMAND_TERMINATED   0x22
+#define TASK_SET_FULL        0x28
+#define ACA_ACTIVE           0x30
+#define TASK_ABORTED         0x40
+
+#define STATUS_MASK          0x3e
+
+/*
+ *  SENSE KEYS
+ */
+
+#define NO_SENSE            0x00
+#define RECOVERED_ERROR     0x01
+#define NOT_READY           0x02
+#define MEDIUM_ERROR        0x03
+#define HARDWARE_ERROR      0x04
+#define ILLEGAL_REQUEST     0x05
+#define UNIT_ATTENTION      0x06
+#define DATA_PROTECT        0x07
+#define BLANK_CHECK         0x08
+#define COPY_ABORTED        0x0a
+#define ABORTED_COMMAND     0x0b
+#define VOLUME_OVERFLOW     0x0d
+#define MISCOMPARE          0x0e
+
+
+/*
+ *  DEVICE TYPES
+ */
+
+#define TYPE_DISK           0x00
+#define TYPE_TAPE           0x01
+#define TYPE_PRINTER        0x02
+#define TYPE_PROCESSOR      0x03    /* HP scanners use this */
+#define TYPE_WORM           0x04    /* Treated as ROM by our system */
+#define TYPE_ROM            0x05
+#define TYPE_SCANNER        0x06
+#define TYPE_MOD            0x07    /* Magneto-optical disk -
+				     * - treated as TYPE_DISK */
+#define TYPE_MEDIUM_CHANGER 0x08
+#define TYPE_STORAGE_ARRAY  0x0c    /* Storage array device */
+#define TYPE_ENCLOSURE      0x0d    /* Enclosure Services Device */
+#define TYPE_RBC            0x0e    /* Simplified Direct-Access Device */
+#define TYPE_OSD            0x11    /* Object-storage Device */
+#define TYPE_WLUN           0x1e    /* Well known LUN */
+#define TYPE_NOT_PRESENT    0x1f
+#define TYPE_INACTIVE       0x20
+#define TYPE_NO_LUN         0x7f
+
+/* Mode page codes for mode sense/set */
+#define MODE_PAGE_R_W_ERROR                   0x01
+#define MODE_PAGE_MRW                         0x03
+#define MODE_PAGE_HD_GEOMETRY                 0x04
+#define MODE_PAGE_FLEXIBLE_DISK_GEOMETRY      0x05 /* SBC */
+#define MODE_PAGE_WRITE_PARAMETER             0x05 /* MMC */
+#define MODE_PAGE_CACHING                     0x08
+#define MODE_PAGE_CD_DEVICE                   0x0d
+#define MODE_PAGE_AUDIO_CTL                   0x0e
+#define MODE_PAGE_POWER                       0x1a
+#define MODE_PAGE_FAULT_FAIL                  0x1c
+#define MODE_PAGE_TO_PROTECT                  0x1d
+#define MODE_PAGE_CAPS_MECH_STATUS            0x2a
+#define MODE_PAGE_MRW_VENDOR                  0x2C
+#define MODE_PAGE_ALLS                        0x3f
+/* Not in Mt. Fuji, but in ATAPI 2.6 -- deprecated now in favor
+ * of MODE_PAGE_SENSE_POWER */
+#define MODE_PAGE_CDROM                       0x0d
+
+#define MODE_PAGE_CDROM                       0x0d
+
+/* Event notification classes for GET EVENT STATUS NOTIFICATION */
+#define GESN_NO_EVENTS                0
+#define GESN_OPERATIONAL_CHANGE       1
+#define GESN_POWER_MANAGEMENT         2
+#define GESN_EXTERNAL_REQUEST         3
+#define GESN_MEDIA                    4
+#define GESN_MULTIPLE_HOSTS           5
+#define GESN_DEVICE_BUSY              6
+
+/* Event codes for MEDIA event status notification */
+#define MEC_NO_CHANGE                 0
+#define MEC_EJECT_REQUESTED           1
+#define MEC_NEW_MEDIA                 2
+#define MEC_MEDIA_REMOVAL             3 /* only for media changers */
+#define MEC_MEDIA_CHANGED             4 /* only for media changers */
+#define MEC_BG_FORMAT_COMPLETED       5 /* MRW or DVD+RW b/g format completed */
+#define MEC_BG_FORMAT_RESTARTED       6 /* MRW or DVD+RW b/g format restarted */
+
+#define MS_TRAY_OPEN                  1
+#define MS_MEDIA_PRESENT              2
+
+/*
+ * Based on values from <linux/cdrom.h> but extending CD_MINS
+ * to the maximum common size allowed by the Orange's Book ATIP
+ *
+ * 90 and 99 min CDs are also available but using them as the
+ * upper limit reduces the effectiveness of the heuristic to
+ * detect DVDs burned to less than 25% of their maximum capacity
+ */
+
+/* Some generally useful CD-ROM information */
+#define CD_MINS                       80 /* max. minutes per CD */
+#define CD_SECS                       60 /* seconds per minute */
+#define CD_FRAMES                     75 /* frames per second */
+#define CD_FRAMESIZE                2048 /* bytes per frame, "cooked" mode */
+#define CD_MAX_BYTES       (CD_MINS * CD_SECS * CD_FRAMES * CD_FRAMESIZE)
+#define CD_MAX_SECTORS     (CD_MAX_BYTES / 512)
+
+/*
+ * The MMC values are not IDE specific and might need to be moved
+ * to a common header if they are also needed for the SCSI emulation
+ */
+
+/* Profile list from MMC-6 revision 1 table 91 */
+#define MMC_PROFILE_NONE                0x0000
+#define MMC_PROFILE_CD_ROM              0x0008
+#define MMC_PROFILE_CD_R                0x0009
+#define MMC_PROFILE_CD_RW               0x000A
+#define MMC_PROFILE_DVD_ROM             0x0010
+#define MMC_PROFILE_DVD_R_SR            0x0011
+#define MMC_PROFILE_DVD_RAM             0x0012
+#define MMC_PROFILE_DVD_RW_RO           0x0013
+#define MMC_PROFILE_DVD_RW_SR           0x0014
+#define MMC_PROFILE_DVD_R_DL_SR         0x0015
+#define MMC_PROFILE_DVD_R_DL_JR         0x0016
+#define MMC_PROFILE_DVD_RW_DL           0x0017
+#define MMC_PROFILE_DVD_DDR             0x0018
+#define MMC_PROFILE_DVD_PLUS_RW         0x001A
+#define MMC_PROFILE_DVD_PLUS_R          0x001B
+#define MMC_PROFILE_DVD_PLUS_RW_DL      0x002A
+#define MMC_PROFILE_DVD_PLUS_R_DL       0x002B
+#define MMC_PROFILE_BD_ROM              0x0040
+#define MMC_PROFILE_BD_R_SRM            0x0041
+#define MMC_PROFILE_BD_R_RRM            0x0042
+#define MMC_PROFILE_BD_RE               0x0043
+#define MMC_PROFILE_HDDVD_ROM           0x0050
+#define MMC_PROFILE_HDDVD_R             0x0051
+#define MMC_PROFILE_HDDVD_RAM           0x0052
+#define MMC_PROFILE_HDDVD_RW            0x0053
+#define MMC_PROFILE_HDDVD_R_DL          0x0058
+#define MMC_PROFILE_HDDVD_RW_DL         0x005A
+#define MMC_PROFILE_INVALID             0xFFFF
+
+#endif
diff --git a/src/usb-backend-common.c b/src/usb-backend-common.c
new file mode 100644
index 0000000..2907cf0
--- /dev/null
+++ b/src/usb-backend-common.c
@@ -0,0 +1,2015 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+    Copyright (C) 2018 Red Hat, Inc.
+
+    Red Hat Authors:
+    Yuri Benditovich<ybendito@redhat.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library 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
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "config.h"
+
+#ifdef USE_USBREDIR
+
+#include <glib-object.h>
+#include <inttypes.h>
+#include <gio/gio.h>
+#include <errno.h>
+#include <libusb.h>
+#include <string.h>
+#include <fcntl.h>
+#include "usbredirhost.h"
+#include "usbredirparser.h"
+#include "spice-util.h"
+#include "usb-backend.h"
+#include "cd-usb-bulk-msd.h"
+#include "cd-device.h"
+#if defined(G_OS_WIN32)
+#include <windows.h>
+#include "win-usb-dev.h"
+#else
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <linux/fs.h>
+#endif
+
+//#define LOUD_DEBUG SPICE_DEBUG
+#define LOUD_DEBUG(x, ...)
+
+//#define INTERCEPT_LOG
+//#define INTERCEPT_LOG2FILE
+#ifdef INTERCEPT_LOG2FILE
+static FILE *fLog;
+#endif
+
+#define MAX_LUN_PER_DEVICE      1
+
+#if MAX_LUN_PER_DEVICE > 1
+#define MAX_OWN_DEVICES         4
+#else
+#define MAX_OWN_DEVICES         8
+#endif
+
+#define OWN_BUS_NUM             0xff
+#define USB2_BCD                0x200
+// TODO: communicate usage of this VID/PID
+#define CD_DEV_VID              0x2b23
+#define CD_DEV_PID              0xCDCD
+#define CD_DEV_CLASS            8
+#define CD_DEV_SUBCLASS         6
+#define CD_DEV_PROTOCOL         0x50
+#define CD_DEV_BLOCK_SIZE       0x200
+#define CD_DEV_MAX_SIZE         737280000
+#define DVD_DEV_BLOCK_SIZE      0x800
+#define MAX_BULK_IN_REQUESTS    64
+
+static void *g_mutex;
+
+struct _SpiceUsbBackendDevice
+{
+    union
+    {
+        void *libusb_device;
+        void *msc;
+    } d;
+    uint32_t isLibUsb   : 1;
+    uint32_t configured : 1;
+    int refCount;
+    void *mutex;
+    SpiceUsbBackendChannel *attached_to;
+    UsbDeviceInformation device_info;
+    SpiceCdLU units[MAX_LUN_PER_DEVICE];
+};
+
+static struct OwnUsbDevices
+{
+    uint32_t active_devices;
+    SpiceUsbBackendDevice devices[MAX_OWN_DEVICES];
+} own_devices;
+
+struct _SpiceUsbBackend
+{
+    libusb_context *libusbContext;
+    usb_hot_plug_callback hp_callback;
+    void *hp_user_data;
+    libusb_hotplug_callback_handle hp_handle;
+    void *dev_change_user_data;
+    backend_device_change_callback dev_change_callback;
+    uint32_t suppressed : 1;
+};
+
+/* backend object for device change notification */
+static SpiceUsbBackend *notify_backend;
+
+struct BufferedBulkRead
+{
+    struct usb_redir_bulk_packet_header hout;
+    uint64_t id;
+};
+
+struct _SpiceUsbBackendChannel
+{
+    struct usbredirhost *usbredirhost;
+    struct usbredirhost *hiddenhost;
+    struct usbredirparser *parser;
+    struct usbredirparser *hiddenparser;
+    uint8_t *read_buf;
+    uint8_t *hello;
+    int read_buf_size;
+    int hello_size;
+    struct usbredirfilter_rule *rules;
+    int rules_count;
+    uint32_t host_caps;
+    uint32_t hello_done_host   : 1;
+    uint32_t hello_done_parser : 1;
+    uint32_t hello_sent        : 1;
+    uint32_t rejected          : 1;
+    SpiceUsbBackendDevice *attached;
+    SpiceUsbBackendChannelInitData data;
+    uint32_t num_reads;
+    struct BufferedBulkRead read_bulk[MAX_BULK_IN_REQUESTS];
+};
+
+static const char *spice_usbutil_libusb_strerror(enum libusb_error error_code)
+{
+    switch (error_code) {
+    case LIBUSB_SUCCESS:
+        return "Success";
+    case LIBUSB_ERROR_IO:
+        return "Input/output error";
+    case LIBUSB_ERROR_INVALID_PARAM:
+        return "Invalid parameter";
+    case LIBUSB_ERROR_ACCESS:
+        return "Access denied (insufficient permissions)";
+    case LIBUSB_ERROR_NO_DEVICE:
+        return "No such device (it may have been disconnected)";
+    case LIBUSB_ERROR_NOT_FOUND:
+        return "Entity not found";
+    case LIBUSB_ERROR_BUSY:
+        return "Resource busy";
+    case LIBUSB_ERROR_TIMEOUT:
+        return "Operation timed out";
+    case LIBUSB_ERROR_OVERFLOW:
+        return "Overflow";
+    case LIBUSB_ERROR_PIPE:
+        return "Pipe error";
+    case LIBUSB_ERROR_INTERRUPTED:
+        return "System call interrupted (perhaps due to signal)";
+    case LIBUSB_ERROR_NO_MEM:
+        return "Insufficient memory";
+    case LIBUSB_ERROR_NOT_SUPPORTED:
+        return "Operation not supported or unimplemented on this platform";
+    case LIBUSB_ERROR_OTHER:
+        return "Other error";
+    }
+    return "Unknown error";
+}
+
+// lock functions for usbredirhost and usbredirparser
+static void *usbredir_alloc_lock(void) {
+    GMutex *mutex;
+
+    mutex = g_new0(GMutex, 1);
+    g_mutex_init(mutex);
+
+    return mutex;
+}
+
+static void usbredir_free_lock(void *user_data) {
+    GMutex *mutex = user_data;
+
+    g_mutex_clear(mutex);
+    g_free(mutex);
+}
+
+static void usbredir_lock_lock(void *user_data) {
+    GMutex *mutex = user_data;
+
+    g_mutex_lock(mutex);
+}
+
+static void usbredir_unlock_lock(void *user_data) {
+    GMutex *mutex = user_data;
+
+    g_mutex_unlock(mutex);
+}
+
+static void indicate_lun_change(SpiceUsbBackend *be, SpiceUsbBackendDevice *bdev)
+{
+    if (be->dev_change_user_data && be->dev_change_callback) {
+        be->dev_change_callback(be->dev_change_user_data, bdev);
+    }
+}
+
+static void indicate_device_presence(SpiceUsbBackend *be, SpiceUsbBackendDevice *bdev, gboolean present)
+{
+#ifdef G_OS_WIN32
+    spice_usb_backend_indicate_dev_change();
+#else
+    if (be->hp_callback) {
+        be->hp_callback(be->hp_user_data, bdev, present);
+    }
+#endif
+}
+
+static gboolean fill_usb_info(SpiceUsbBackendDevice *bdev)
+{
+    UsbDeviceInformation *pi = &bdev->device_info;
+
+    if (bdev->isLibUsb)
+    {
+        struct libusb_device_descriptor desc;
+        libusb_device *libdev = bdev->d.libusb_device;
+        int res = libusb_get_device_descriptor(libdev, &desc);
+        pi->bus = libusb_get_bus_number(libdev);
+        pi->address = libusb_get_device_address(libdev);
+        if (res < 0) {
+            g_warning("cannot get device descriptor for (%p) %d.%d",
+                libdev, pi->bus, pi->address);
+            return FALSE;
+        }
+        pi->vid = desc.idVendor;
+        pi->pid = desc.idProduct;
+        pi->class = desc.bDeviceClass;
+        pi->subclass = desc.bDeviceSubClass;
+        pi->protocol = desc.bDeviceProtocol;
+        pi->isochronous = 0;
+        pi->max_luns = 0;
+    }
+    return TRUE;
+}
+
+static gboolean open_stream(SpiceCdLU *unit, const char *filename)
+{
+    gboolean b = FALSE;
+    b = cd_device_open_stream(unit, filename) == 0;
+    return b;
+}
+
+static void close_stream(SpiceCdLU *unit)
+{
+    if (unit->stream) {
+        g_object_unref(unit->stream);
+        unit->stream = NULL;
+    }
+    if (unit->file_object) {
+        g_object_unref(unit->file_object);
+        unit->file_object = NULL;
+    }
+}
+
+/* Note that this function must be re-entrant safe, as it can get called
+from both the main thread as well as from the usb event handling thread */
+static void usbredir_write_flush_callback(void *user_data)
+{
+    SpiceUsbBackendChannel *ch = user_data;
+    gboolean b = ch->data.is_channel_ready(ch->data.user_data);
+    if (b) {
+        if (ch->usbredirhost) {
+            SPICE_DEBUG("%s ch %p -> usbredirhost", __FUNCTION__, ch);
+            usbredirhost_write_guest_data(ch->usbredirhost);
+        }
+        else if (ch->parser) {
+            SPICE_DEBUG("%s ch %p -> usbredirparser", __FUNCTION__, ch);
+            usbredirparser_do_write(ch->parser);
+        }
+        else {
+            b = FALSE;
+        }
+    }
+
+    if (!b) {
+        SPICE_DEBUG("%s ch %p (not ready)", __FUNCTION__, ch);
+    }
+}
+
+void cd_usb_bulk_msd_read_complete(void *user_data,
+    uint8_t *data, uint32_t length, CdUsbBulkStatus status)
+{
+    SpiceUsbBackendDevice *d = (SpiceUsbBackendDevice *)user_data;
+    SpiceUsbBackendChannel *ch = d->attached_to;
+    if (ch && ch->attached == d && ch->parser) {
+        int nread;
+        uint32_t offset = 0;
+
+        for (nread = 0; nread <ch->num_reads; nread++) {
+            uint32_t max_len;
+            max_len = (ch->read_bulk[nread].hout.length_high << 16) |
+                        ch->read_bulk[nread].hout.length;
+            if (max_len > length) {
+                max_len = length;
+                ch->read_bulk[nread].hout.length = length;
+                ch->read_bulk[nread].hout.length_high = length >> 16;
+            }
+
+            switch (status) {
+            case BULK_STATUS_GOOD:
+                ch->read_bulk[nread].hout.status = usb_redir_success;
+                break;
+            case BULK_STATUS_CANCELED:
+                ch->read_bulk[nread].hout.status = usb_redir_cancelled;
+                break;
+            case BULK_STATUS_ERROR:
+                ch->read_bulk[nread].hout.status = usb_redir_ioerror;
+                break;
+            case BULK_STATUS_STALL:
+            default:
+                ch->read_bulk[nread].hout.status = usb_redir_stall;
+                break;
+            }
+
+            SPICE_DEBUG("%s: responding %" PRIu64 " with len %u out of %u, status %d",
+                __FUNCTION__, ch->read_bulk[nread].id, max_len,
+                length, ch->read_bulk[nread].hout.status);
+            usbredirparser_send_bulk_packet(ch->parser, ch->read_bulk[nread].id,
+                &ch->read_bulk[nread].hout, max_len ? (data + offset) : NULL, max_len);
+
+            offset += max_len;
+            length -= max_len;
+        }
+        ch->num_reads = 0;
+        usbredir_write_flush_callback(ch);
+
+        if (length) {
+            SPICE_DEBUG("%s: ERROR: %u bytes were not reported!", __FUNCTION__, length);
+        }
+
+    } else {
+        SPICE_DEBUG("%s: broken device<->channel relationship!", __FUNCTION__);
+    }
+}
+
+/* device reset completion callback */
+void cd_usb_bulk_msd_reset_complete(void *user_data, int status)
+{
+    // SpiceUsbBackendDevice *d = (SpiceUsbBackendDevice *)user_data;
+}
+
+static gboolean load_lun(SpiceUsbBackendDevice *d, int unit, gboolean load)
+{
+    gboolean b = TRUE;
+    if (load && d->units[unit].device) {
+        // there is one possible problem in case our backend is the
+        // local CD device and it is ejected
+        cd_device_load(&d->units[unit], TRUE);
+        close_stream(&d->units[unit]);
+        if (cd_device_check(&d->units[unit]) || !open_stream(&d->units[unit], NULL)) {
+            return FALSE;
+        }
+    }
+
+    if (load) {
+        CdScsiMediaParameters media_params = { 0 };
+
+        media_params.stream = d->units[unit].stream;
+        media_params.size = d->units[unit].size;
+        media_params.block_size = d->units[unit].blockSize;
+        if (media_params.block_size == CD_DEV_BLOCK_SIZE &&
+            media_params.size % DVD_DEV_BLOCK_SIZE == 0) {
+            media_params.block_size = DVD_DEV_BLOCK_SIZE;
+        }
+        SPICE_DEBUG("%s: loading %s, size %" PRIu64 ", block %u",
+            __FUNCTION__, d->units[unit].filename, media_params.size, media_params.block_size);
+
+        b = !cd_usb_bulk_msd_load(d->d.msc, unit, &media_params);
+
+        d->units[unit].loaded = !!b;
+
+    } else {
+        SPICE_DEBUG("%s: unloading %s", __FUNCTION__, d->units[unit].filename);
+        cd_usb_bulk_msd_unload(d->d.msc, unit);
+        d->units[unit].loaded = FALSE;
+// I do not see why we need to eject the physical CD-ROM when the eject
+// on VM requested. On Linux this may require root privilege, as this
+// affects those who opened files on CD-ROM. I suppose this is user's
+// responsibility to keep CD door close when it shares it with VM
+#if DO_CD_DEVICE_EJECT
+        cd_device_load(&d->units[unit], FALSE);
+#endif
+    }
+    return b;
+}
+
+static gboolean lock_lun(SpiceUsbBackendDevice *d, int unit, gboolean lock)
+{
+    SPICE_DEBUG("%s: locking %s", __FUNCTION__, d->units[unit].filename);
+    return !cd_usb_bulk_msd_lock(d->d.msc, unit, lock);
+}
+
+// called when a change happens on SCSI layer
+void cd_usb_bulk_msd_lun_changed(void *user_data, uint32_t lun)
+{
+    SpiceUsbBackendDevice *d = (SpiceUsbBackendDevice *)user_data;
+    CdScsiDeviceInfo cd_info;
+
+    if (!cd_usb_bulk_msd_get_info(d->d.msc, lun, &cd_info)) {
+        // load or unload command received from SCSI
+        if (d->units[lun].loaded != cd_info.loaded) {
+            if (!load_lun(d, lun, cd_info.loaded)) {
+                SPICE_DEBUG("%s: load failed, unloading unit", __FUNCTION__);
+                cd_usb_bulk_msd_unload(d->d.msc, lun);
+            }
+        }
+    }
+
+    if (notify_backend) {
+        indicate_lun_change(notify_backend, d);
+    }
+}
+
+static void process_default_parameters(CdScsiDeviceParameters *params,
+    const SpiceUsbDeviceLunInfo *info, int unit)
+{
+    if (!params->product || *params->product == 0) {
+        const char *name = strrchr(info->file_path, '\\');
+        if (!name) name = strrchr(info->file_path, '/');
+        params->product = name ? (name + 1) : info->file_path;
+    }
+    if (!params->version || *params->version == 0) {
+        static char s[8];
+        g_snprintf(s, sizeof(s), "%d", unit);
+        params->version = s;
+    }
+    if (!params->vendor || *params->vendor == 0) {
+        params->vendor = "SPICE";
+    }
+}
+
+static gboolean activate_device(SpiceUsbBackendDevice *d, const SpiceUsbDeviceLunInfo *info, int unit)
+{
+    gboolean b = FALSE;
+    CdScsiDeviceParameters dev_params = { 0 };
+    dev_params.vendor = info->vendor;
+    dev_params.product = info->product;
+    dev_params.version = info->revision;
+
+    process_default_parameters(&dev_params, info, unit);
+
+    if (!d->d.msc) {
+        d->d.msc = cd_usb_bulk_msd_alloc(d, MAX_LUN_PER_DEVICE);
+        if (!d->d.msc) {
+            return FALSE;
+        }
+    }
+    d->units[unit].blockSize = CD_DEV_BLOCK_SIZE;
+    b = !cd_usb_bulk_msd_realize(d->d.msc, unit, &dev_params);
+    if (b) {
+        b = open_stream(&d->units[unit], info->file_path);
+        if (b && info->loaded) {
+            b = load_lun(d, unit, TRUE);
+            if (b && info->locked) {
+                b = lock_lun(d, unit, TRUE);
+            }
+        }
+        if (!b) {
+            close_stream(&d->units[unit]);
+            cd_usb_bulk_msd_unrealize(d->d.msc, unit);
+        }
+    }
+    return b;
+}
+
+static gboolean stop_device(SpiceUsbBackendDevice *d, int unit)
+{
+    int j;
+    gboolean empty = TRUE;
+
+    cd_usb_bulk_msd_unrealize(d->d.msc, unit);
+    if (d->units[unit].filename) {
+        g_free(d->units[unit].filename);
+        d->units[unit].filename = NULL;
+    }
+    close_stream(&d->units[unit]);
+
+    for (j = 0; empty && j < MAX_LUN_PER_DEVICE; ++j) {
+        if (d->units[j].filename) {
+            empty = FALSE;
+        }
+    }
+    if (empty) {
+        cd_usb_bulk_msd_free(d->d.msc);
+        d->d.msc = NULL;
+        d->configured = 0;
+    }
+    return empty;
+}
+
+gboolean spice_usb_backend_add_cd_lun(SpiceUsbBackend *be, const SpiceUsbDeviceLunInfo *info)
+{
+    int i;
+    gboolean b = FALSE;
+
+    if (be->suppressed) {
+        SPICE_DEBUG("%s: CD sharing is suppressed", __FUNCTION__);
+        return FALSE;
+    }
+
+    for (i = 0; !b && i < MAX_OWN_DEVICES; i++) {
+        if ((1 << i) & ~own_devices.active_devices) { /* inactive usb device */
+            SPICE_DEBUG("%s: add file %s to device %d (activate now) as lun 0",
+                        __FUNCTION__, info->file_path, i);
+
+            b = activate_device(&own_devices.devices[i], info, 0);
+            if (b) {
+                own_devices.active_devices |= 1 << i;
+                indicate_device_presence(be, &own_devices.devices[i], TRUE);
+            }
+        } else if (!own_devices.devices[i].attached_to) {
+            /* active unattached device, add as the next lun */
+            int j;
+            for (j = 0; j < MAX_LUN_PER_DEVICE; j++) {
+                if (!own_devices.devices[i].units[j].stream) {
+                    SPICE_DEBUG("%s: add file %s to device %d (already active) as lun %d",
+                                __FUNCTION__, info->file_path, i, j);
+
+                    b = activate_device(&own_devices.devices[i], info, j);
+                    if (!b) {
+                        SPICE_DEBUG("%s: failed to add file %s to device %d (already active) as lun %d",
+                                    __FUNCTION__, info->file_path, i, j);
+                        b = TRUE; /* exit outer loop */
+                    } else {
+                        indicate_lun_change(be, &own_devices.devices[i]);
+                    }
+                    break;
+                }
+            }
+        }
+    }
+    if (!b) {
+        SPICE_DEBUG("can not create device %s", info->file_path);
+    }
+    return b;
+}
+
+static gboolean check_device(SpiceUsbBackendDevice *bdev, guint lun, guint* index)
+{
+    int i;
+
+    if (lun >= MAX_LUN_PER_DEVICE) {
+        return FALSE;
+    }
+
+    for (i = 0; i < MAX_OWN_DEVICES; i++) {
+        if ((1 << i) & own_devices.active_devices) { /* active usb device */
+            if (&own_devices.devices[i] == bdev) {
+                if (index) {
+                    *index = i;
+                }
+                return TRUE;
+            }
+        }
+    }
+
+    return FALSE;
+}
+
+gboolean spice_usb_backend_remove_cd_lun(SpiceUsbBackend *be, SpiceUsbBackendDevice *bdev, guint lun)
+{
+    char *name;
+    guint index;
+    if (!check_device(bdev, lun, &index)) {
+        return FALSE;
+    }
+
+    name = bdev->units[lun].filename;
+    SPICE_DEBUG("%s: unshare %s, unit %u:%u", __FUNCTION__, name, index, lun);
+    if (stop_device(bdev, lun)) {
+        /* usb device does not have any active unit,
+        so we remove it */
+        own_devices.active_devices &= ~(1 << index);
+        indicate_device_presence(be, bdev, FALSE);
+    }
+    else {
+        /* the device still contains active LUN(s) */
+        indicate_lun_change(be, bdev);
+    }
+    return TRUE;
+}
+
+uint32_t spice_usb_backend_get_cd_luns_bitmask(SpiceUsbBackendDevice *bdev)
+{
+    int i, value = 0;
+
+    if (!check_device(bdev, 0, NULL)) {
+        return FALSE;
+    }
+
+    for (i = 0; i < MAX_LUN_PER_DEVICE; i++) {
+        char *name = bdev->units[i].filename;
+        if (name) {
+            value |= 1 << i;
+        }
+    }
+
+    return value;
+}
+
+gboolean spice_usb_backend_get_cd_lun_info(SpiceUsbBackendDevice *bdev,
+    guint lun, SpiceUsbDeviceLunInfo *info)
+{
+    if (!check_device(bdev, lun, NULL)) {
+        return FALSE;
+    }
+
+    if (bdev->units[lun].filename) {
+        CdScsiDeviceInfo cd_info;
+        if (!cd_usb_bulk_msd_get_info(bdev->d.msc, lun, &cd_info)) {
+            info->started = cd_info.started;
+            info->loaded = bdev->units[lun].loaded;
+            info->locked = cd_info.locked;
+            info->file_path = bdev->units[lun].filename;
+            info->vendor = cd_info.parameters.vendor;
+            info->product = cd_info.parameters.product;
+            info->revision = cd_info.parameters.version;
+            return TRUE;
+        }
+    }
+
+    return FALSE;
+}
+
+gboolean spice_usb_backend_load_cd_lun(
+    SpiceUsbBackend *be,
+    SpiceUsbBackendDevice *bdev,
+    guint lun,
+    gboolean load)
+{
+    if (!check_device(bdev, lun, NULL)) {
+        return FALSE;
+    }
+
+    if (bdev->units[lun].filename) {
+        gboolean b = load_lun(bdev, lun, load);
+        SPICE_DEBUG("%s: %sload %s", __FUNCTION__,
+            load ? "" : "un", b ? "succeeded" : "failed");
+        if (b) {
+            indicate_lun_change(be, bdev);
+        }
+        return b;
+    }
+
+    return FALSE;
+}
+
+gboolean spice_usb_backend_lock_cd_lun(
+    SpiceUsbBackend *be,
+    SpiceUsbBackendDevice *bdev,
+    guint lun,
+    gboolean lock)
+{
+    if (!check_device(bdev, lun, NULL)) {
+        return FALSE;
+    }
+
+    if (bdev->units[lun].filename) {
+        gboolean b = lock_lun(bdev, lun, lock);
+        SPICE_DEBUG("%s: %slock %s", __FUNCTION__,
+            lock ? "" : "un", b ? "succeeded" : "failed");
+        if (b) {
+            indicate_lun_change(be, bdev);
+        }
+        return b;
+    }
+
+    return FALSE;
+}
+
+gboolean spice_usb_backend_change_cd_lun(
+    SpiceUsbBackend *be,
+    SpiceUsbBackendDevice *bdev,
+    guint lun, const SpiceUsbDeviceLunInfo *lun_info)
+{
+    gboolean b = FALSE;
+
+    char *filename;
+    GFile *file_object;
+    GFileInputStream *stream;
+
+    if (!check_device(bdev, lun, NULL)) {
+        return b;
+    }
+
+    // if the CD is loaded, we need to unload it first
+    if (bdev->units[lun].loaded) {
+        SPICE_DEBUG("%s: the unit is loaded, unload it first", __FUNCTION__);
+        return b;
+    }
+
+    // keep stream-related data (stream, file object)
+    filename = bdev->units[lun].filename;
+    bdev->units[lun].filename = NULL;
+
+    stream = bdev->units[lun].stream;
+    bdev->units[lun].stream = NULL;
+
+    file_object = bdev->units[lun].file_object;
+    bdev->units[lun].file_object = NULL;
+
+    // now we can start the LUN with new parameters
+    b = open_stream(&bdev->units[lun], lun_info->file_path);
+    if (b) {
+        b = load_lun(bdev, lun, TRUE);
+    }
+    else {
+        close_stream(&bdev->units[lun]);
+    }
+
+    if (!b) {
+        SPICE_DEBUG("%s: failed", __FUNCTION__);
+        // restore the state of unloaded unit
+        bdev->units[lun].filename = filename;
+        bdev->units[lun].stream = stream;
+        bdev->units[lun].file_object = file_object;
+    }
+    else {
+        SPICE_DEBUG("%s: succeeded", __FUNCTION__);
+        indicate_lun_change(be, bdev);
+    }
+
+    return b;
+}
+
+static void initialize_own_devices(void)
+{
+    int i;
+    // addresses 0 and 1 excluded as they are treated as
+    // not suitable for redirection
+    for (i = 0; i < MAX_OWN_DEVICES; i++) {
+        own_devices.devices[i].mutex = g_mutex;
+        own_devices.devices[i].device_info.bus = OWN_BUS_NUM;
+        own_devices.devices[i].device_info.address = i + 2;
+        own_devices.devices[i].device_info.vid = CD_DEV_VID;
+        own_devices.devices[i].device_info.pid = CD_DEV_PID;
+        own_devices.devices[i].device_info.class = 0;
+        own_devices.devices[i].device_info.subclass = 0;
+        own_devices.devices[i].device_info.protocol = 0;
+        own_devices.devices[i].device_info.max_luns = MAX_LUN_PER_DEVICE;
+    }
+}
+
+#ifdef INTERCEPT_LOG
+static void log_handler(
+    const gchar *log_domain,
+    GLogLevelFlags log_level,
+    const gchar *message,
+    gpointer user_data)
+{
+    GString *log_msg;
+    log_msg = g_string_new(NULL);
+    if (log_msg)
+    {
+        gchar *timestamp;
+        GThread *th = g_thread_self();
+        GDateTime *current_time = g_date_time_new_now_local();
+        gint micros = g_date_time_get_microsecond(current_time);
+        timestamp = g_date_time_format(current_time, "%H:%M:%S");
+        g_string_append_printf(log_msg, "[%p][%s.%03d]", th, timestamp, micros / 1000);
+        g_date_time_unref(current_time);
+        g_free(timestamp);
+        g_string_append(log_msg, message);
+#ifdef INTERCEPT_LOG2FILE
+        g_string_append(log_msg, "\n");
+        fwrite(log_msg->str, 1, strlen(log_msg->str), fLog);
+#else
+        g_log_default_handler(log_domain, log_level, log_msg->str, NULL);
+#endif
+        g_string_free(log_msg, TRUE);
+    }
+}
+#endif
+
+static void configure_log(void)
+{
+#ifdef INTERCEPT_LOG2FILE
+    fLog = fopen("remote-viewer.log", "w+t");
+#endif
+#ifdef INTERCEPT_LOG
+    g_log_set_default_handler(log_handler, NULL);
+#endif
+}
+
+SpiceUsbBackend *spice_usb_backend_initialize(void)
+{
+    SpiceUsbBackend *be;
+    SPICE_DEBUG("%s >>", __FUNCTION__);
+    if (!g_mutex) {
+        g_mutex = usbredir_alloc_lock();
+        initialize_own_devices();
+        configure_log();
+    }
+    be = (SpiceUsbBackend *)g_new0(SpiceUsbBackend, 1);
+    if (be) {
+#ifndef USE_CD_SHARING
+        be->suppressed = TRUE;
+#endif
+        int rc;
+        rc = libusb_init(&be->libusbContext);
+        if (rc < 0) {
+            const char *desc = spice_usbutil_libusb_strerror(rc);
+            g_warning("Error initializing LIBUSB support: %s [%i]", desc, rc);
+        } else {
+#ifdef G_OS_WIN32
+#if LIBUSB_API_VERSION >= 0x01000106
+    libusb_set_option(be->libusbContext, LIBUSB_OPTION_USE_USBDK);
+#endif
+#endif
+        }
+    }
+    SPICE_DEBUG("%s <<", __FUNCTION__);
+    return be;
+}
+
+gboolean spice_usb_backend_handle_events(SpiceUsbBackend *be)
+{
+    SPICE_DEBUG("%s >>", __FUNCTION__);
+    gboolean b = TRUE;
+    if (be->libusbContext) {
+        SPICE_DEBUG("%s >> libusb", __FUNCTION__);
+        int res = libusb_handle_events(be->libusbContext);
+        if (res && res != LIBUSB_ERROR_INTERRUPTED) {
+            const char *desc = spice_usbutil_libusb_strerror(res);
+            g_warning("Error handling USB events: %s [%i]", desc, res);
+            b = FALSE;
+        }
+        SPICE_DEBUG("%s << libusb %d", __FUNCTION__, res);
+    }
+    else {
+        b = TRUE;
+        g_usleep(1000000);
+    }
+    SPICE_DEBUG("%s <<", __FUNCTION__);
+    return b;
+}
+
+static int LIBUSB_CALL hotplug_callback(libusb_context *ctx,
+    libusb_device *device,
+    libusb_hotplug_event event,
+    void *user_data)
+{
+    SpiceUsbBackend *be = (SpiceUsbBackend *)user_data;
+    if (be->hp_callback) {
+        SpiceUsbBackendDevice *d;
+        gboolean val = event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED;
+        d = g_new0(SpiceUsbBackendDevice, 1);
+        if (d) {
+            d->isLibUsb = 1;
+            d->refCount = 1;
+            d->mutex = g_mutex;
+            d->d.libusb_device = device;
+            if (fill_usb_info(d)) {
+                SPICE_DEBUG("created dev %p, usblib dev %p", d, device);
+                be->hp_callback(be->hp_user_data, d, val);
+            } else {
+                g_free(d);
+            }
+        }
+    }
+    return 0;
+}
+
+gboolean spice_usb_backend_handle_hotplug(
+    SpiceUsbBackend *be,
+    void *user_data,
+    usb_hot_plug_callback proc)
+{
+    int rc;
+    if (!proc) {
+        if (be->hp_handle) {
+            libusb_hotplug_deregister_callback(be->libusbContext, be->hp_handle);
+            be->hp_handle = 0;
+        }
+        be->hp_callback = proc;
+        return TRUE;
+    }
+
+    be->hp_callback = proc;
+    be->hp_user_data = user_data;
+    if (!be->libusbContext) {
+        // it is acceptable if libusb is not available at all
+        return TRUE;
+    }
+    rc = libusb_hotplug_register_callback(be->libusbContext,
+        LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
+        LIBUSB_HOTPLUG_ENUMERATE, LIBUSB_HOTPLUG_MATCH_ANY,
+        LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY,
+        hotplug_callback, be, &be->hp_handle);
+    if (rc != LIBUSB_SUCCESS) {
+        const char *desc = spice_usbutil_libusb_strerror(rc);
+        g_warning("Error initializing USB hotplug support: %s [%i]", desc, rc);
+        be->hp_callback = NULL;
+        return FALSE;
+    }
+    return TRUE;
+}
+
+void spice_usb_backend_set_device_change_callback(
+    SpiceUsbBackend *be, void *user_data, backend_device_change_callback proc)
+{
+    be->dev_change_user_data = user_data;
+    be->dev_change_callback = proc;
+    if (!notify_backend) {
+        notify_backend = be;
+    }
+}
+
+void spice_usb_backend_finalize(SpiceUsbBackend *be)
+{
+    SPICE_DEBUG("%s >>", __FUNCTION__);
+    if (be->libusbContext) {
+        libusb_exit(be->libusbContext);
+    }
+    if (be == notify_backend) {
+        notify_backend = NULL;
+    }
+    g_free(be);
+    SPICE_DEBUG("%s <<", __FUNCTION__);
+}
+
+SpiceUsbBackendDevice **spice_usb_backend_get_device_list(SpiceUsbBackend *be)
+{
+    LOUD_DEBUG("%s >>", __FUNCTION__);
+    libusb_device **devlist = NULL, **dev;
+    SpiceUsbBackendDevice *d, **list;
+
+    int n = 0, i, index;
+
+    if (be->libusbContext) {
+        libusb_get_device_list(be->libusbContext, &devlist);
+    }
+
+    // add all the libusb device that not present in our list
+    for (dev = devlist; dev && *dev; dev++) {
+        n++;
+    }
+
+    list = g_new0(SpiceUsbBackendDevice*, n + MAX_OWN_DEVICES);
+    if (!list) {
+        libusb_free_device_list(devlist, 1);
+        return NULL;
+    }
+
+    for (i = 0; i < MAX_OWN_DEVICES; ++i) {
+        if (own_devices.active_devices & (1 << i)) {
+            n++;
+        }
+    }
+
+    index = 0;
+
+    for (dev = devlist; dev && *dev; dev++) {
+        d = g_new0(SpiceUsbBackendDevice, 1);
+        if (d) {
+            d->isLibUsb = 1;
+            d->refCount = 1;
+            d->mutex = g_mutex;
+            d->d.libusb_device = *dev;
+            if (index >= n || !fill_usb_info(d)) {
+                g_free(d);
+                libusb_unref_device(*dev);
+            }
+            else {
+                SPICE_DEBUG("created dev %p, usblib dev %p", d, *dev);
+                list[index++] = d;
+            }
+        }
+    }
+
+    usbredir_lock_lock(g_mutex);
+
+    for (i = 0; i < MAX_OWN_DEVICES; ++i) {
+        d = &own_devices.devices[i];
+        if ((own_devices.active_devices & (1 << i)) && index < n) {
+            list[index++] = d;
+            d->refCount++;
+            SPICE_DEBUG("found own %p, address %d", d, d->device_info.address);
+        }
+    }
+    usbredir_unlock_lock(g_mutex);
+
+    if (devlist) {
+        libusb_free_device_list(devlist, 0);
+    }
+
+    LOUD_DEBUG("%s <<", __FUNCTION__);
+    return list;
+}
+
+gboolean spice_usb_backend_device_is_hub(SpiceUsbBackendDevice *dev)
+{
+    return dev->device_info.class == LIBUSB_CLASS_HUB;
+}
+
+static uint8_t is_libusb_isochronous(libusb_device *libdev)
+{
+    struct libusb_config_descriptor *conf_desc;
+    uint8_t isoc_found = FALSE;
+    gint i, j, k;
+
+    if (!libdev) {
+        SPICE_DEBUG("%s - unexpected libdev = 0", __FUNCTION__);
+        return 0;
+    }
+
+    if (libusb_get_active_config_descriptor(libdev, &conf_desc) != 0) {
+        SPICE_DEBUG("%s - no active configuration for libdev %p", __FUNCTION__, libdev);
+        return 0;
+    }
+
+    for (i = 0; !isoc_found && i < conf_desc->bNumInterfaces; i++) {
+        for (j = 0; !isoc_found && j < conf_desc->interface[i].num_altsetting; j++) {
+            for (k = 0; !isoc_found && k < conf_desc->interface[i].altsetting[j].bNumEndpoints;k++) {
+                gint attributes = conf_desc->interface[i].altsetting[j].endpoint[k].bmAttributes;
+                gint type = attributes & LIBUSB_TRANSFER_TYPE_MASK;
+                if (type == LIBUSB_TRANSFER_TYPE_ISOCHRONOUS)
+                    isoc_found = TRUE;
+            }
+        }
+    }
+
+    libusb_free_config_descriptor(conf_desc);
+    return isoc_found;
+}
+
+const UsbDeviceInformation*  spice_usb_backend_device_get_info(SpiceUsbBackendDevice *dev)
+{
+    dev->device_info.isochronous = dev->isLibUsb ? is_libusb_isochronous(dev->d.libusb_device) : 0;
+    return &dev->device_info;
+}
+
+gboolean spice_usb_backend_devices_same(
+    SpiceUsbBackendDevice *dev1,
+    SpiceUsbBackendDevice *dev2)
+{
+    if (dev1->isLibUsb != dev2->isLibUsb) {
+        return FALSE;
+    }
+    if (dev1->isLibUsb) {
+        return dev1->d.libusb_device == dev2->d.libusb_device;
+    }
+    // assuming CD redir devices are static
+    return dev1 == dev2;
+}
+
+gconstpointer spice_usb_backend_device_get_libdev(SpiceUsbBackendDevice *dev)
+{
+    if (dev->isLibUsb) {
+        return dev->d.libusb_device;
+    }
+    return NULL;
+}
+
+void spice_usb_backend_free_device_list(SpiceUsbBackendDevice **devlist)
+{
+    LOUD_DEBUG("%s >>", __FUNCTION__);
+    SpiceUsbBackendDevice **dev;
+    for (dev = devlist; *dev; dev++) {
+        SpiceUsbBackendDevice *d = *dev;
+        spice_usb_backend_device_release(d);
+    }
+    g_free(devlist);
+    LOUD_DEBUG("%s <<", __FUNCTION__);
+}
+
+void spice_usb_backend_device_acquire(SpiceUsbBackendDevice *dev)
+{
+    void *mutex = dev->mutex;
+    LOUD_DEBUG("%s >> %p", __FUNCTION__, dev);
+    usbredir_lock_lock(mutex);
+    if (dev->isLibUsb) {
+        libusb_ref_device(dev->d.libusb_device);
+    }
+    dev->refCount++;
+    usbredir_unlock_lock(mutex);
+}
+
+void spice_usb_backend_device_release(SpiceUsbBackendDevice *dev)
+{
+    void *mutex = dev->mutex;
+    LOUD_DEBUG("%s >> %p(%d)", __FUNCTION__, dev, dev->refCount);
+    usbredir_lock_lock(mutex);
+    if (dev->isLibUsb) {
+        libusb_unref_device(dev->d.libusb_device);
+        dev->refCount--;
+        if (dev->refCount == 0) {
+            LOUD_DEBUG("%s freeing %p (libusb %p)", __FUNCTION__, dev, dev->d.libusb_device);
+            g_free(dev);
+        }
+    }
+    else {
+        dev->refCount--;
+    }
+    usbredir_unlock_lock(mutex);
+    LOUD_DEBUG("%s <<", __FUNCTION__);
+}
+
+gboolean spice_usb_backend_device_need_thread(SpiceUsbBackendDevice *dev)
+{
+    gboolean b = dev->isLibUsb != 0;
+    SPICE_DEBUG("%s << %d", __FUNCTION__, b);
+    return b;
+}
+
+int spice_usb_backend_device_check_filter(
+    SpiceUsbBackendDevice *dev,
+    const struct usbredirfilter_rule *rules,
+    int count)
+{
+    if (dev->isLibUsb) {
+        return usbredirhost_check_device_filter(
+            rules, count, dev->d.libusb_device, 0);
+    } else {
+        uint8_t cls, subcls, proto;
+        cls = CD_DEV_CLASS;
+        subcls = CD_DEV_SUBCLASS;
+        proto = CD_DEV_PROTOCOL;
+        return usbredirfilter_check(rules, count,
+            dev->device_info.class,
+            dev->device_info.subclass,
+            dev->device_info.protocol,
+            &cls, &subcls, &proto, 1, dev->device_info.vid,
+            dev->device_info.pid, USB2_BCD, 0);
+    }
+}
+
+static int usbredir_read_callback(void *user_data, uint8_t *data, int count)
+{
+    SpiceUsbBackendChannel *ch = user_data;
+
+    count = MIN(ch->read_buf_size, count);
+
+    if (count != 0) {
+        memcpy(data, ch->read_buf, count);
+    }
+
+    ch->read_buf_size -= count;
+    if (ch->read_buf_size) {
+        ch->read_buf += count;
+    }
+    else {
+        ch->read_buf = NULL;
+    }
+    SPICE_DEBUG("%s ch %p, %d bytes", __FUNCTION__, ch, count);
+
+    return count;
+}
+
+static void usbredir_log(void *user_data, int level, const char *msg)
+{
+    SpiceUsbBackendChannel *ch = (SpiceUsbBackendChannel *)user_data;
+
+    switch (level) {
+    case usbredirparser_error:
+        g_critical("%s", msg);
+        ch->data.log(ch->data.user_data, msg, TRUE);
+        break;
+    case usbredirparser_warning:
+        g_warning("%s", msg);
+        ch->data.log(ch->data.user_data, msg, TRUE);
+        break;
+    default:
+        ch->data.log(ch->data.user_data, msg, FALSE);
+        break;
+    }
+}
+
+static int usbredir_write_callback(void *user_data, uint8_t *data, int count)
+{
+    SpiceUsbBackendChannel *ch = user_data;
+    int res;
+    SPICE_DEBUG("%s ch %p, %d bytes", __FUNCTION__, ch, count);
+    if (!ch->hello_sent) {
+        ch->hello_sent = 1;
+        if (count == 80) {
+            memcpy(&ch->host_caps, data + 76, 4);
+            SPICE_DEBUG("%s ch %p, sending first hello, caps %08X",
+                __FUNCTION__, ch, ch->host_caps);
+        }
+    }
+    res = ch->data.write_callback(ch->data.user_data, data, count);
+    return res;
+}
+
+#if USBREDIR_VERSION >= 0x000701
+static uint64_t usbredir_buffered_output_size_callback(void *user_data)
+{
+    SpiceUsbBackendChannel *ch = user_data;
+    return ch->data.get_queue_size(ch->data.user_data);
+}
+#endif
+
+int spice_usb_backend_provide_read_data(SpiceUsbBackendChannel *ch, uint8_t *data, int count)
+{
+    int res = 0;
+    if (!ch->read_buf) {
+        typedef int(*readproc_t)(void *);
+        readproc_t fn = NULL;
+        void *param;
+        ch->read_buf = data;
+        ch->read_buf_size = count;
+        if (!ch->hello) {
+            ch->hello = g_malloc(count);
+            memcpy(ch->hello, data, count);
+            ch->hello_size = count;
+            if (ch->usbredirhost) {
+                ch->hello_done_host = 1;
+            }
+            if (ch->parser) {
+                ch->hello_done_parser = 1;
+            }
+        }
+        if (ch->usbredirhost) {
+            fn = (readproc_t)usbredirhost_read_guest_data;
+            param = ch->usbredirhost;
+        }
+        if (ch->parser) {
+            fn = (readproc_t)usbredirparser_do_read;
+            param = ch->parser;
+        }
+        res = fn ? fn(param) : USB_REDIR_ERROR_IO;
+        switch (res)
+        {
+        case usbredirhost_read_io_error:
+            res = USB_REDIR_ERROR_IO;
+            break;
+        case usbredirhost_read_parse_error:
+            res = USB_REDIR_ERROR_READ_PARSE;
+            break;
+        case usbredirhost_read_device_rejected:
+            res = USB_REDIR_ERROR_DEV_REJECTED;
+            break;
+        case usbredirhost_read_device_lost:
+            res = USB_REDIR_ERROR_DEV_LOST;
+            break;
+        }
+        SPICE_DEBUG("%s ch %p, %d bytes, res %d", __FUNCTION__, ch, count, res);
+
+    } else {
+        res = USB_REDIR_ERROR_READ_PARSE;
+        SPICE_DEBUG("%s ch %p, %d bytes, already has data", __FUNCTION__, ch, count);
+    }
+    if (ch->rejected) {
+        ch->rejected = 0;
+        res = USB_REDIR_ERROR_DEV_REJECTED;
+    }
+    return res;
+}
+
+void spice_usb_backend_return_write_data(SpiceUsbBackendChannel *ch, void *data)
+{
+    typedef void(*retdata)(void *, void *);
+    retdata fn = NULL;
+    void *param;
+    if (ch->usbredirhost) {
+        fn = (retdata)usbredirhost_free_write_buffer;
+        param = ch->usbredirhost;
+    }
+    if (ch->parser) {
+        fn = (retdata)usbredirhost_free_write_buffer;
+        param = ch->parser;
+    }
+    if (fn) {
+        SPICE_DEBUG("%s ch %p", __FUNCTION__, ch);
+        fn(param, data);
+    } else {
+        SPICE_DEBUG("%s ch %p - NOBODY TO CALL", __FUNCTION__, ch);
+    }
+}
+
+#if 0
+struct usbredirparser {
+    /* app private data passed into all callbacks as the priv argument */
+    void *priv;
+    /* non packet callbacks */
+    usbredirparser_log log_func;
+    usbredirparser_read read_func;
+    usbredirparser_write write_func;
+    /* usb-redir-protocol v0.3 control packet complete callbacks */
+    usbredirparser_device_connect device_connect_func;
+    usbredirparser_device_disconnect device_disconnect_func;
+    usbredirparser_reset reset_func;
+    usbredirparser_interface_info interface_info_func;
+    usbredirparser_ep_info ep_info_func;
+    usbredirparser_set_configuration set_configuration_func;
+    usbredirparser_get_configuration get_configuration_func;
+    usbredirparser_configuration_status configuration_status_func;
+    usbredirparser_set_alt_setting set_alt_setting_func;
+    usbredirparser_get_alt_setting get_alt_setting_func;
+    usbredirparser_alt_setting_status alt_setting_status_func;
+    usbredirparser_start_iso_stream start_iso_stream_func;
+    usbredirparser_stop_iso_stream stop_iso_stream_func;
+    usbredirparser_iso_stream_status iso_stream_status_func;
+    usbredirparser_start_interrupt_receiving start_interrupt_receiving_func;
+    usbredirparser_stop_interrupt_receiving stop_interrupt_receiving_func;
+    usbredirparser_interrupt_receiving_status interrupt_receiving_status_func;
+    usbredirparser_alloc_bulk_streams alloc_bulk_streams_func;
+    usbredirparser_free_bulk_streams free_bulk_streams_func;
+    usbredirparser_bulk_streams_status bulk_streams_status_func;
+    usbredirparser_cancel_data_packet cancel_data_packet_func;
+    /* usb-redir-protocol v0.3 data packet complete callbacks */
+    usbredirparser_control_packet control_packet_func;
+    usbredirparser_bulk_packet bulk_packet_func;
+    usbredirparser_iso_packet iso_packet_func;
+    usbredirparser_interrupt_packet interrupt_packet_func;
+    /* usbredir 0.3.2 new non packet callbacks (for multi-thread locking) */
+    usbredirparser_alloc_lock alloc_lock_func;
+    usbredirparser_lock lock_func;
+    usbredirparser_unlock unlock_func;
+    usbredirparser_free_lock free_lock_func;
+    /* usbredir 0.3.2 new control packet complete callbacks */
+    usbredirparser_hello hello_func;
+    /* usbredir 0.4 new control packet complete callbacks */
+    usbredirparser_filter_reject filter_reject_func;
+    usbredirparser_filter_filter filter_filter_func;
+    usbredirparser_device_disconnect_ack device_disconnect_ack_func;
+    /* usbredir 0.6 new control packet complete callbacks */
+    usbredirparser_start_bulk_receiving start_bulk_receiving_func;
+    usbredirparser_stop_bulk_receiving stop_bulk_receiving_func;
+    usbredirparser_bulk_receiving_status bulk_receiving_status_func;
+    /* usbredir 0.6 new data packet complete callbacks */
+    usbredirparser_buffered_bulk_packet buffered_bulk_packet_func;
+};
+#endif
+
+static void *get_device_string(SpiceUsbBackendDevice *d, uint16_t index, uint8_t *len)
+{
+    static uint16_t s0[2] = { 0x304, 0x409 };
+    static uint16_t s1[8] = { 0x310, 'R', 'e', 'd', ' ', 'H', 'a', 't' };
+    static uint16_t s2[9] = { 0x312, 'S', 'p', 'i', 'c', 'e', ' ', 'C', 'D' };
+    static uint16_t s3[4] = { 0x308, 'X', '0', '0' };
+    void *p = NULL;
+    switch (index)
+    {
+        case 0:
+            p = s0; *len = sizeof(s0); break;
+        case 1:
+            p = s1; *len = sizeof(s1); break;
+        case 2:
+            p = s2; *len = sizeof(s2); break;
+        case 3:
+            s3[2] = '0' + d->device_info.address / 10;
+            s3[3] = '0' + d->device_info.address % 10;
+            p = s3; *len = sizeof(s3); break;
+    }
+    return p;
+}
+
+static void get_device_descriptor(SpiceUsbBackendDevice *d,
+    struct usb_redir_control_packet_header *h)
+{
+    uint8_t *buffer = (uint8_t *)(h + 1);
+    const void *p = NULL;
+    uint8_t len = 0;
+    static const struct libusb_device_descriptor desc =
+    {
+        .bLength = 18,
+        .bDescriptorType = LIBUSB_DT_DEVICE,
+        .bcdUSB = USB2_BCD,
+        .bDeviceClass = 0,
+        .bDeviceSubClass = 0,
+        .bDeviceProtocol = 0,
+        .bMaxPacketSize0 = 64,
+        .idVendor = CD_DEV_VID,
+        .idProduct = CD_DEV_PID,
+        .bcdDevice = 0x100,
+        .iManufacturer = 1,
+        .iProduct = 2,
+        .iSerialNumber = 3,
+        .bNumConfigurations = 1
+    };
+    static const uint8_t cfg[] =
+    {
+        9, //len of cfg desc
+        LIBUSB_DT_CONFIG, // desc type
+        0x20, // wlen
+        0,
+        1, // num if
+        1, // cfg val
+        0, // cfg name
+        0x80, // bus powered
+        0x32, // 100 ma
+        9, // len of IF desc
+        LIBUSB_DT_INTERFACE,
+        0, // num if
+        0, // alt setting
+        2, // num of endpoints
+        CD_DEV_CLASS,
+        CD_DEV_SUBCLASS,
+        CD_DEV_PROTOCOL,
+        0, // if name
+        7,
+        LIBUSB_DT_ENDPOINT,
+        0x81, //->Direction : IN - EndpointID : 1
+        0x02, //->Bulk Transfer Type
+        0,    //wMaxPacketSize : 0x0200 = 0x200 max bytes
+        2,
+        0,    //bInterval
+        7,
+        LIBUSB_DT_ENDPOINT,
+        0x02, //->Direction : OUT - EndpointID : 2
+        0x02, //->Bulk Transfer Type
+        0,    //wMaxPacketSize : 0x0200 = 0x200 max bytes
+        2,
+        0,    //bInterval
+    };
+    switch (h->value >> 8) {
+        case LIBUSB_DT_DEVICE:
+            p = &desc;
+            len = sizeof(desc);
+            break;
+        case LIBUSB_DT_CONFIG:
+            p = cfg;
+            len = sizeof(cfg);
+            break;
+        case LIBUSB_DT_STRING:
+            p = get_device_string(d, h->value & 0xff, &len);
+            break;
+    }
+    if (p && len) {
+        len = MIN(len, h->length);
+        memcpy(buffer, p, len);
+        h->length = len;
+    } else {
+        h->length = 0;
+        h->status = usb_redir_stall;
+    }
+}
+
+static void usbredir_control_packet(void *priv,
+    uint64_t id, struct usb_redir_control_packet_header *h,
+    uint8_t *data, int data_len)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    struct {
+        struct usb_redir_control_packet_header h;
+        uint8_t buffer[255];
+    } response = { 0 };
+    uint8_t reqtype = h->requesttype & 0x7f;
+    response.h = *h;
+    response.h.status = 0;
+    SPICE_DEBUG("%s %p: TRVIL %02X %02X %04X %04X %04X",
+        __FUNCTION__,
+        ch, h->requesttype, h->request,
+        h->value, h->index, h->length);
+    if (!ch->attached) {
+        // device already detached
+        response.h.status = usb_redir_ioerror;
+        response.h.length = 0;
+    } else if (reqtype == (LIBUSB_REQUEST_TYPE_STANDARD | LIBUSB_RECIPIENT_DEVICE)) {
+        switch (h->request) {
+            case LIBUSB_REQUEST_GET_DESCRIPTOR:
+                get_device_descriptor(ch->attached, &response.h);
+                break;
+            default:
+                response.h.length = 0;
+                break;
+        }
+    } else if (reqtype == (LIBUSB_REQUEST_TYPE_STANDARD | LIBUSB_RECIPIENT_ENDPOINT)) {
+        // should be clear stall request
+        response.h.length = 0;
+        response.h.status = 0;
+    } else if (reqtype == (LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE)) {
+        response.h.length = 0;
+        response.h.status = 0;
+        switch (h->request) {
+            case 0xFF:
+                // mass-storage class request 'reset'
+                break;
+            case 0xFE:
+                // mass-storage class request 'get max lun'
+                // returning one byte
+                if (!h->length) {
+                    response.h.status = usb_redir_stall;
+                } else {
+                    response.h.length = 1;
+                    response.buffer[0] = MAX_LUN_PER_DEVICE - 1;
+                }
+                break;
+            default:
+                break;
+        }
+    }
+    else {
+     response.h.length = 0;
+     response.h.status = usb_redir_stall;
+ }
+    SPICE_DEBUG("%s responding with payload of %02X, status %X",
+        __FUNCTION__,
+        response.h.length, response.h.status);
+    usbredirparser_send_control_packet(ch->parser, id, &response.h,
+        response.h.length ? response.buffer : NULL, response.h.length);
+    usbredir_write_flush_callback(ch);
+    usbredirparser_free_packet_data(ch->parser, data);
+}
+
+static void usbredir_bulk_packet(void *priv,
+    uint64_t id, struct usb_redir_bulk_packet_header *h,
+    uint8_t *data, int data_len)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SpiceUsbBackendDevice *d = ch->attached;
+    struct usb_redir_bulk_packet_header hout = *h;
+    uint32_t len = (h->length_high << 16) | h->length;
+    SPICE_DEBUG("%s %p: ep %X, len %u, id %" PRIu64, __FUNCTION__,
+                ch, h->endpoint, len, id);
+    if (!d || !d->d.msc) {
+        SPICE_DEBUG("%s: device not attached or not realized", __FUNCTION__);
+        hout.status = usb_redir_ioerror;
+        hout.length = hout.length_high = 0;
+        SPICE_DEBUG("%s: responding (a) with ZLP status %d", __FUNCTION__, hout.status);
+        usbredirparser_send_bulk_packet(ch->parser, id,
+            &hout, NULL, 0);
+    } else if (h->endpoint & LIBUSB_ENDPOINT_IN) {
+        if (ch->num_reads < MAX_BULK_IN_REQUESTS) {
+            int res;
+            if (ch->num_reads) {
+                SPICE_DEBUG("%s: already has %u pending reads", __FUNCTION__, ch->num_reads);
+            }
+            ch->read_bulk[ch->num_reads].hout = *h;
+            ch->read_bulk[ch->num_reads].id = id;
+            ch->num_reads++;
+            res = cd_usb_bulk_msd_read(d->d.msc, len);
+            if (res) {
+                SPICE_DEBUG("%s: error on bulk read", __FUNCTION__);
+                ch->num_reads--;
+                hout.length = hout.length_high = 0;
+                hout.status = usb_redir_ioerror;
+                SPICE_DEBUG("%s: responding (b) with ZLP status %d", __FUNCTION__, hout.status);
+                usbredirparser_send_bulk_packet(ch->parser, id, &hout, NULL, 0);
+            }
+
+        } else {
+            SPICE_DEBUG("%s: too many pending reads", __FUNCTION__);
+            hout.status = usb_redir_babble;
+            hout.length = hout.length_high = 0;
+            SPICE_DEBUG("%s: responding (a) with ZLP status %d", __FUNCTION__, hout.status);
+            usbredirparser_send_bulk_packet(ch->parser, id, &hout, NULL, 0);
+        }
+    } else {
+        cd_usb_bulk_msd_write(d->d.msc, data, data_len);
+        hout.status = usb_redir_success;
+        SPICE_DEBUG("%s: responding status %d", __FUNCTION__, hout.status);
+        usbredirparser_send_bulk_packet(ch->parser, id, &hout, NULL, 0);
+    }
+
+    usbredirparser_free_packet_data(ch->parser, data);
+    usbredir_write_flush_callback(ch);
+}
+
+static void usbredir_device_reset(void *priv)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SPICE_DEBUG("%s ch %p", __FUNCTION__, ch);
+    if (ch->attached) {
+        cd_usb_bulk_msd_reset(ch->attached->d.msc);
+    }
+}
+
+static void usbredir_interface_info(void *priv,
+    struct usb_redir_interface_info_header *interface_info)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SPICE_DEBUG("%s not implemented %p", __FUNCTION__, ch);
+}
+
+static void usbredir_interface_ep_info(void *priv,
+    struct usb_redir_ep_info_header *ep_info)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SPICE_DEBUG("%s not implemented %p", __FUNCTION__, ch);
+}
+
+static void usbredir_set_configuration(void *priv,
+    uint64_t id, struct usb_redir_set_configuration_header *set_configuration)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    struct usb_redir_configuration_status_header h;
+    h.status = 0;
+    h.configuration = set_configuration->configuration;
+    SPICE_DEBUG("%s ch %p, cfg %d", __FUNCTION__, ch, h.configuration);
+    if (ch->attached) {
+        ch->attached->configured = h.configuration != 0;
+    }
+    usbredirparser_send_configuration_status(ch->parser, id, &h);
+    usbredir_write_flush_callback(ch);
+}
+
+static void usbredir_get_configuration(void *priv, uint64_t id)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    struct usb_redir_configuration_status_header h;
+    h.status = 0;
+    h.configuration = ch->attached && ch->attached->configured;
+    SPICE_DEBUG("%s ch %p, cfg %d", __FUNCTION__, ch, h.configuration);
+    usbredirparser_send_configuration_status(ch->parser, id, &h);
+    usbredir_write_flush_callback(ch);
+}
+
+static void usbredir_set_alt_setting(void *priv,
+    uint64_t id, struct usb_redir_set_alt_setting_header *s)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    struct usb_redir_alt_setting_status_header sh;
+    sh.status = (!s->interface && !s->alt) ? 0 : usb_redir_stall;
+    sh.interface = s->interface;
+    sh.alt = s->alt;
+    SPICE_DEBUG("%s ch %p, %d:%d", __FUNCTION__, ch, s->interface, s->alt);
+    usbredirparser_send_alt_setting_status(ch->parser, id, &sh);
+    usbredir_write_flush_callback(ch);
+}
+
+static void usbredir_get_alt_setting(void *priv,
+    uint64_t id, struct usb_redir_get_alt_setting_header *s)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    struct usb_redir_alt_setting_status_header sh;
+    sh.status = (s->interface == 0) ? 0 : usb_redir_stall;
+    sh.interface = s->interface;
+    sh.alt = 0;
+    SPICE_DEBUG("%s ch %p, if %d", __FUNCTION__, ch, s->interface);
+    usbredirparser_send_alt_setting_status(ch->parser, id, &sh);
+    usbredir_write_flush_callback(ch);
+}
+
+static void usbredir_cancel_data(void *priv, uint64_t id)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SpiceUsbBackendDevice *d = ch->attached;
+    int nread, found = -1;
+    SPICE_DEBUG("%s ch %p id %" PRIu64 ", num_reads %u",
+        __FUNCTION__, ch, id, ch->num_reads);
+
+    if (!d || !d->d.msc) {
+        SPICE_DEBUG("%s: device not attached or not realized", __FUNCTION__);
+        return;
+    }
+
+    for (nread = 0; nread < ch->num_reads; nread++) {
+        if (ch->read_bulk[nread].id == id) {
+            found = nread;
+            break;
+        }
+    }
+    if (found >= 0) {
+        if (cd_usb_bulk_msd_cancel_read(d->d.msc)) {
+            cd_usb_bulk_msd_read_complete(d, NULL, 0, BULK_STATUS_CANCELED);
+        }
+    } else {
+        SPICE_DEBUG("%s: ERROR: no such id to cancel!", __FUNCTION__);
+    }
+}
+
+static void usbredir_filter_reject(void *priv)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SPICE_DEBUG("%s %p", __FUNCTION__, ch);
+    ch->rejected = 1;
+}
+
+/* Note that the ownership of the rules array is passed on to the callback. */
+static void usbredir_filter_filter(void *priv,
+    struct usbredirfilter_rule *r, int count)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SPICE_DEBUG("%s ch %p %d filters", __FUNCTION__, ch, count);
+    ch->rules = r;
+    ch->rules_count = count;
+    if (count) {
+        int i;
+        for (i = 0; i < count; i++) {
+            SPICE_DEBUG("%s class %d, %X:%X",
+                r[i].allow ? "allowed" : "denied", r[i].device_class,
+                (uint32_t)r[i].vendor_id, (uint32_t)r[i].product_id);
+        }
+    }
+}
+
+static void usbredir_device_disconnect_ack(void *priv)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    SPICE_DEBUG("%s ch %p", __FUNCTION__, ch);
+    if (ch->parser) {
+        ch->parser = NULL;
+        SPICE_DEBUG("%s switch to usbredirhost", __FUNCTION__);
+        ch->usbredirhost = ch->hiddenhost;
+    }
+}
+
+static void usbredir_hello(void *priv,
+    struct usb_redir_hello_header *hello)
+{
+    SpiceUsbBackendChannel *ch = priv;
+    struct usb_redir_device_connect_header device_connect;
+    struct usb_redir_ep_info_header ep_info = { 0 };
+    struct usb_redir_interface_info_header interface_info = { 0 };
+    SPICE_DEBUG("%s %p %sattached %s", __FUNCTION__, ch,
+        ch->attached ? "" : "not ",  hello ? "" : "(internal)");
+    if (ch->attached) {
+        interface_info.interface_count = 1;
+        interface_info.interface_class[0] = CD_DEV_CLASS;
+        interface_info.interface_subclass[0] = CD_DEV_SUBCLASS;
+        interface_info.interface_protocol[0] = CD_DEV_PROTOCOL;
+        usbredirparser_send_interface_info(ch->parser, &interface_info);
+
+        ep_info.type[0x11] = 2;
+        ep_info.max_packet_size[0x11] = 512;
+        ep_info.type[0x02] = 2;
+        ep_info.max_packet_size[0x02] = 512;
+        usbredirparser_send_ep_info(ch->parser, &ep_info);
+
+        device_connect.device_class = 0;
+        device_connect.device_subclass = 0;
+        device_connect.device_protocol = 0;
+        device_connect.vendor_id = ch->attached->device_info.vid;
+        device_connect.product_id = ch->attached->device_info.pid;
+        device_connect.device_version_bcd = USB2_BCD;
+        device_connect.speed = usb_redir_speed_high;
+        usbredirparser_send_device_connect(ch->parser, &device_connect);
+        usbredir_write_flush_callback(ch);
+    }
+}
+
+/*
+    We initialize the usbredirparser with HELLO enabled only in case
+    the libusb is not active and the usbredirhost does not function.
+    Then the parser sends session HELLO and receives server's response.
+    Otherwise (usbredirparser initialized with HELLO disabled):
+    - the usbredirhost sends session HELLO
+    - we look into it to know set of capabilities we shall initialize
+      the parser with
+    - we cache server's response to HELLO and provide it to parser on
+      first activation (attach of emulated device) to have it synchronized
+      with server's capabilities
+*/
+static struct usbredirparser *create_parser(SpiceUsbBackendChannel *ch, gboolean bHello)
+{
+    struct usbredirparser *parser = usbredirparser_create();
+    if (parser) {
+        uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0 };
+        uint32_t flags = bHello ? 0 : usbredirparser_fl_no_hello;
+        flags |= usbredirparser_fl_write_cb_owns_buffer |
+            usbredirparser_fl_usb_host;
+        parser->priv = ch;
+        parser->log_func = usbredir_log;
+        parser->read_func = usbredir_read_callback;
+        parser->write_func = usbredir_write_callback;
+        parser->reset_func = usbredir_device_reset;
+        parser->interface_info_func = usbredir_interface_info;
+        parser->ep_info_func = usbredir_interface_ep_info;
+        parser->set_configuration_func = usbredir_set_configuration;
+        parser->get_configuration_func = usbredir_get_configuration;
+        parser->set_alt_setting_func = usbredir_set_alt_setting;
+        parser->get_alt_setting_func = usbredir_get_alt_setting;
+        parser->cancel_data_packet_func = usbredir_cancel_data;
+        parser->control_packet_func = usbredir_control_packet;
+        parser->bulk_packet_func = usbredir_bulk_packet;
+        parser->alloc_lock_func = usbredir_alloc_lock;
+        parser->lock_func = usbredir_lock_lock;
+        parser->unlock_func = usbredir_unlock_lock;
+        parser->free_lock_func = usbredir_free_lock;
+        parser->hello_func = usbredir_hello;
+        parser->filter_reject_func = usbredir_filter_reject;
+        parser->filter_filter_func = usbredir_filter_filter;
+        parser->device_disconnect_ack_func = usbredir_device_disconnect_ack;
+
+        if (bHello) {
+            ch->hello_sent = 1;
+            ch->host_caps |= 1 << usb_redir_cap_connect_device_version;
+            ch->host_caps |= 1 << usb_redir_cap_device_disconnect_ack;
+            ch->host_caps |= 1 << usb_redir_cap_ep_info_max_packet_size;
+            ch->host_caps |= 1 << usb_redir_cap_64bits_ids;
+            ch->host_caps |= 1 << usb_redir_cap_32bits_bulk_length;
+        }
+
+        if (ch->host_caps & (1 << usb_redir_cap_connect_device_version)) {
+            usbredirparser_caps_set_cap(caps, usb_redir_cap_connect_device_version);
+        }
+        usbredirparser_caps_set_cap(caps, usb_redir_cap_filter);
+        if (ch->host_caps & (1 << usb_redir_cap_device_disconnect_ack)) {
+            usbredirparser_caps_set_cap(caps, usb_redir_cap_device_disconnect_ack);
+        }
+        if (ch->host_caps & (1 << usb_redir_cap_ep_info_max_packet_size)) {
+            usbredirparser_caps_set_cap(caps, usb_redir_cap_ep_info_max_packet_size);
+        }
+        if (ch->host_caps & (1 << usb_redir_cap_64bits_ids)) {
+            usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids);
+        }
+        if (ch->host_caps & (1 << usb_redir_cap_32bits_bulk_length)) {
+            usbredirparser_caps_set_cap(caps, usb_redir_cap_32bits_bulk_length);
+        }
+        if (ch->host_caps & (1 << usb_redir_cap_bulk_streams)) {
+            usbredirparser_caps_set_cap(caps, usb_redir_cap_bulk_streams);
+        }
+        usbredirparser_init(parser, PACKAGE_STRING, caps, USB_REDIR_CAPS_SIZE, flags);
+    }
+
+    return parser;
+}
+
+gboolean spice_usb_backend_channel_attach(SpiceUsbBackendChannel *ch, SpiceUsbBackendDevice *dev, const char **msg)
+{
+    const char *dummy;
+    if (!msg) {
+        msg = &dummy;
+    }
+    SPICE_DEBUG("%s >> ch %p, dev %p (was attached %p)", __FUNCTION__, ch, dev, ch->attached);
+    gboolean b = FALSE;
+    if (dev && dev->isLibUsb) {
+        libusb_device_handle *handle = NULL;
+        int rc = libusb_open(dev->d.libusb_device, &handle);
+        b = rc == 0 && handle;
+        if (b) {
+            if (!ch->usbredirhost) {
+                ch->usbredirhost = ch->hiddenhost;
+                ch->parser = NULL;
+            }
+            rc = usbredirhost_set_device(ch->usbredirhost, handle);
+            if (rc) {
+                SPICE_DEBUG("%s ch %p, dev %p usbredirhost error %d", __FUNCTION__, ch, dev, rc);
+                b = FALSE;
+            } else {
+                ch->attached = dev;
+                dev->attached_to = ch;
+                if (ch->hello && !ch->hello_done_host) {
+                    SPICE_DEBUG("%s sending cached hello to host", __FUNCTION__);
+                    ch->hello_done_host = 1;
+                    spice_usb_backend_provide_read_data(ch, ch->hello, ch->hello_size);
+                }
+            }
+        } else {
+            const char *desc = spice_usbutil_libusb_strerror(rc);
+            g_warning("Error libusb_open: %s [%i]", desc, rc);
+            *msg = desc;
+        }
+    } else if (!dev) {
+        SPICE_DEBUG("%s intentional sleep", __FUNCTION__);
+        g_usleep(100000);
+        if (ch->usbredirhost) {
+            // it will call libusb_close internally
+            usbredirhost_set_device(ch->usbredirhost, NULL);
+        } else {
+            // CD redir detach
+            usbredirparser_send_device_disconnect(ch->parser);
+            usbredir_write_flush_callback(ch);
+        }
+        SPICE_DEBUG("%s ch %p, detach done", __FUNCTION__, ch);
+        ch->attached->attached_to = NULL;
+        ch->attached = NULL;
+        b = TRUE;
+    } else {
+        // CD redir attach
+        b = TRUE;
+        ch->usbredirhost = NULL;
+        ch->parser = ch->hiddenparser;
+        ch->attached = dev;
+        dev->attached_to = ch;
+        if (ch->hello_done_parser) {
+            // send device info
+            usbredir_hello(ch, NULL);
+        } else if (ch->hello) {
+            SPICE_DEBUG("%s sending cached hello to parser", __FUNCTION__);
+            ch->hello_done_parser = 1;
+            spice_usb_backend_provide_read_data(ch, ch->hello, ch->hello_size);
+            usbredir_write_flush_callback(ch);
+        }
+    }
+    return b;
+}
+
+SpiceUsbBackendChannel *spice_usb_backend_channel_initialize(
+    SpiceUsbBackend *be,
+    const SpiceUsbBackendChannelInitData *init_data)
+{
+    SpiceUsbBackendChannel *ch = g_new0(SpiceUsbBackendChannel, 1);
+    SPICE_DEBUG("%s >>", __FUNCTION__);
+    gboolean ok = FALSE;
+    if (ch) {
+        ch->data = *init_data;
+        ch->hiddenhost = !be->libusbContext ? NULL :
+            usbredirhost_open_full(
+                be->libusbContext,
+                NULL,
+                usbredir_log,
+                usbredir_read_callback,
+                usbredir_write_callback,
+                usbredir_write_flush_callback,
+                usbredir_alloc_lock,
+                usbredir_lock_lock,
+                usbredir_unlock_lock,
+                usbredir_free_lock,
+                ch, PACKAGE_STRING,
+                init_data->debug ? usbredirparser_debug : usbredirparser_warning,
+                usbredirhost_fl_write_cb_owns_buffer);
+        ok = be->libusbContext == NULL || ch->hiddenhost != NULL;
+        if (ch->hiddenhost) {
+#if USBREDIR_VERSION >= 0x000701
+            usbredirhost_set_buffered_output_size_cb(ch->hiddenhost, usbredir_buffered_output_size_callback);
+#endif
+        }
+    }
+
+    if (ok) {
+        ch->usbredirhost = ch->hiddenhost;
+    }
+
+    if (ch && !ok) {
+        g_error("Out of memory allocating usbredir or parser");
+        if (ch->hiddenhost) {
+            usbredirhost_close(ch->hiddenhost);
+        }
+        g_free(ch);
+        ch = NULL;
+    }
+    SPICE_DEBUG("%s << %p", __FUNCTION__, ch);
+    return ch;
+}
+
+void spice_usb_backend_channel_up(SpiceUsbBackendChannel *ch)
+{
+    SPICE_DEBUG("%s %p, host %p", __FUNCTION__, ch, ch->usbredirhost);
+    if (ch->usbredirhost) {
+        usbredirhost_write_guest_data(ch->usbredirhost);
+        ch->hiddenparser = create_parser(ch, FALSE);
+    } else if (!ch->hiddenparser) {
+        ch->hiddenparser = create_parser(ch, TRUE);
+        ch->parser = ch->hiddenparser;
+        usbredirparser_do_write(ch->parser);
+    }
+}
+
+void spice_usb_backend_channel_finalize(SpiceUsbBackendChannel *ch)
+{
+    SPICE_DEBUG("%s >> %p", __FUNCTION__, ch);
+    if (ch->usbredirhost) {
+        usbredirhost_close(ch->usbredirhost);
+    }
+    if (ch->parser) {
+        usbredirparser_destroy(ch->parser);
+    }
+    if (ch->hello) {
+        g_free(ch->hello);
+    }
+
+    if (ch->rules) {
+        // is it ok to g_free the memory that was allocated by parser?
+        g_free(ch->rules);
+    }
+
+    g_free(ch);
+    SPICE_DEBUG("%s << %p", __FUNCTION__, ch);
+}
+
+void spice_usb_backend_channel_get_guest_filter(
+    SpiceUsbBackendChannel *ch,
+    const struct usbredirfilter_rule **r,
+    int *count)
+{
+    int i;
+    *r = NULL;
+    *count = 0;
+    if (ch->usbredirhost) {
+        usbredirhost_get_guest_filter(ch->usbredirhost, r, count);
+    }
+    if (*r == NULL) {
+        *r = ch->rules;
+        *count = ch->rules_count;
+    }
+
+    if (*count) {
+        SPICE_DEBUG("%s ch %p: %d filters", __FUNCTION__, ch, *count);
+    }
+    for (i = 0; i < *count; i++) {
+        const struct usbredirfilter_rule *ra = *r;
+        SPICE_DEBUG("%s class %d, %X:%X",
+            ra[i].allow ? "allowed" : "denied", ra[i].device_class,
+            (uint32_t)ra[i].vendor_id, (uint32_t)ra[i].product_id);
+    }
+}
+
+gboolean  spice_usb_backend_device_get_info_by_address(guint8 bus, guint8 addr, UsbDeviceInformation *info)
+{
+    int i;
+    if (bus != OWN_BUS_NUM) {
+        return FALSE;
+    }
+    for (i = 0; i < MAX_OWN_DEVICES; i++) {
+        if (own_devices.devices[i].device_info.address == addr) {
+            *info = own_devices.devices[i].device_info;
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+#endif // USB_REDIR
diff --git a/src/usb-backend.h b/src/usb-backend.h
new file mode 100644
index 0000000..47ec2b3
--- /dev/null
+++ b/src/usb-backend.h
@@ -0,0 +1,112 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+    Copyright (C) 2018 Red Hat, Inc.
+
+    Red Hat Authors:
+    Yuri Benditovich<ybendito@redhat.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library 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
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef __SPICE_USB_BACKEND_H__
+#define __SPICE_USB_BACKEND_H__
+
+#include <usbredirfilter.h>
+#include "usb-device-manager.h"
+
+G_BEGIN_DECLS
+
+typedef struct _SpiceUsbBackend SpiceUsbBackend;
+typedef struct _SpiceUsbBackendDevice SpiceUsbBackendDevice;
+typedef struct _SpiceUsbBackendChannel SpiceUsbBackendChannel;
+
+typedef struct _UsbDeviceInformation
+{
+    uint16_t bus;
+    uint16_t address;
+    uint16_t vid;
+    uint16_t pid;
+    uint8_t class;
+    uint8_t subclass;
+    uint8_t protocol;
+    uint8_t isochronous;
+    uint8_t max_luns;
+} UsbDeviceInformation;
+
+typedef struct _SpiceUsbBackendChannelInitData
+{
+    void *user_data;
+    void (*log)(void *user_data, const char *msg, gboolean error);
+    int (*write_callback)(void *user_data, uint8_t *data, int count);
+    int (*is_channel_ready)(void *user_data);
+    uint64_t (*get_queue_size)(void *user_data);
+    gboolean debug;
+} SpiceUsbBackendChannelInitData;
+
+typedef void(*usb_hot_plug_callback)(
+    void *user_data, SpiceUsbBackendDevice *dev, gboolean added);
+
+typedef void(*backend_device_change_callback)(
+    void *user_data, SpiceUsbBackendDevice *dev);
+
+enum {
+    USB_REDIR_ERROR_IO = -1,
+    USB_REDIR_ERROR_READ_PARSE = -2,
+    USB_REDIR_ERROR_DEV_REJECTED = -3,
+    USB_REDIR_ERROR_DEV_LOST = -4,
+};
+
+SpiceUsbBackend *spice_usb_backend_initialize(void);
+gboolean spice_usb_backend_handle_events(SpiceUsbBackend *);
+gboolean spice_usb_backend_handle_hotplug(SpiceUsbBackend *, void *user_data, usb_hot_plug_callback proc);
+void spice_usb_backend_set_device_change_callback(SpiceUsbBackend *, void *user_data, backend_device_change_callback proc);
+void spice_usb_backend_finalize(SpiceUsbBackend *context);
+// returns NULL-terminated array of SpiceUsbBackendDevice *
+SpiceUsbBackendDevice **spice_usb_backend_get_device_list(SpiceUsbBackend *backend);
+gboolean spice_usb_backend_device_is_hub(SpiceUsbBackendDevice *dev);
+gboolean spice_usb_backend_device_need_thread(SpiceUsbBackendDevice *dev);
+void spice_usb_backend_free_device_list(SpiceUsbBackendDevice **devlist);
+void spice_usb_backend_device_acquire(SpiceUsbBackendDevice *dev);
+void spice_usb_backend_device_release(SpiceUsbBackendDevice *dev);
+gboolean spice_usb_backend_devices_same(SpiceUsbBackendDevice *dev1, SpiceUsbBackendDevice *dev2);
+gconstpointer spice_usb_backend_device_get_libdev(SpiceUsbBackendDevice *dev);
+const UsbDeviceInformation*  spice_usb_backend_device_get_info(SpiceUsbBackendDevice *dev);
+gboolean  spice_usb_backend_device_get_info_by_address(guint8 bus, guint8 addr, UsbDeviceInformation *info);
+// returns 0 if the device passes the filter
+int spice_usb_backend_device_check_filter(SpiceUsbBackendDevice *dev, const struct usbredirfilter_rule *rules, int count);
+
+SpiceUsbBackendChannel *spice_usb_backend_channel_initialize(SpiceUsbBackend *context, const SpiceUsbBackendChannelInitData *init_data);
+// returns 0 for success or error code
+int spice_usb_backend_provide_read_data(SpiceUsbBackendChannel *ch, uint8_t *data, int count);
+gboolean spice_usb_backend_channel_attach(SpiceUsbBackendChannel *ch, SpiceUsbBackendDevice *dev, const char **msg);
+void spice_usb_backend_channel_up(SpiceUsbBackendChannel *ch);
+void spice_usb_backend_channel_get_guest_filter(SpiceUsbBackendChannel *ch, const struct usbredirfilter_rule  **rules, int *count);
+void spice_usb_backend_return_write_data(SpiceUsbBackendChannel *ch, void *data);
+void spice_usb_backend_channel_finalize(SpiceUsbBackendChannel *ch);
+
+gboolean spice_usb_backend_add_cd_lun(SpiceUsbBackend *be, const SpiceUsbDeviceLunInfo *info);
+gboolean spice_usb_backend_remove_cd_lun(SpiceUsbBackend *be, SpiceUsbBackendDevice *bdev, guint lun);
+uint32_t spice_usb_backend_get_cd_luns_bitmask(SpiceUsbBackendDevice *bdev);
+gboolean spice_usb_backend_get_cd_lun_info(SpiceUsbBackendDevice *bdev, guint lun, SpiceUsbDeviceLunInfo *info);
+gboolean spice_usb_backend_load_cd_lun(SpiceUsbBackend *be,
+    SpiceUsbBackendDevice *bdev, guint lun, gboolean load);
+gboolean spice_usb_backend_lock_cd_lun(SpiceUsbBackend *be,
+    SpiceUsbBackendDevice *bdev, guint lun, gboolean lock);
+gboolean spice_usb_backend_change_cd_lun(
+    SpiceUsbBackend *be, SpiceUsbBackendDevice *bdev,
+    guint lun, const SpiceUsbDeviceLunInfo *info);
+
+G_END_DECLS
+
+#endif
diff --git a/src/usb-device-redir-widget.c b/src/usb-device-redir-widget.c
new file mode 100644
index 0000000..59a6043
--- /dev/null
+++ b/src/usb-device-redir-widget.c
@@ -0,0 +1,2065 @@ 
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   Copyright (C) 2012 Red Hat, Inc.
+
+   Red Hat Authors:
+   Alexander Nezhinsky<anezhins@redhat.com>
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   This library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "config.h"
+#ifndef USB_WIDGET_TEST
+    #include <glib/gi18n-lib.h>
+    #include "spice-client.h"
+    #include "spice-marshal.h"
+#else
+    #include "spice-client.h"
+#endif
+#include "usb-device-widget.h"
+
+/*
+    Debugging note:
+    Logging from this module is not affected by --spice-debug
+    command line parameter
+    Use SPICE_DEBUG=1 environment varible to enable logs
+*/
+
+#ifdef USE_NEW_USB_WIDGET
+
+/**
+ * SECTION:usb-device-widget
+ * @short_description: USB device selection widget
+ * @title: Spice USB device selection widget
+ * @section_id:
+ * @see_also:
+ * @stability: Under development
+ * @include: spice-client-gtk.h
+ *
+ * #SpiceUsbDeviceWidget is a gtk widget which apps can use to easily
+ * add an UI to select USB devices to redirect (or unredirect).
+ */
+
+struct _SpiceUsbDeviceWidget
+{
+    GtkBox parent;
+
+    SpiceUsbDeviceWidgetPrivate *priv;
+};
+
+struct _SpiceUsbDeviceWidgetClass
+{
+    GtkBoxClass parent_class;
+
+    /* signals */
+    void (*connect_failed) (SpiceUsbDeviceWidget *widget,
+                            SpiceUsbDevice *device, GError *error);
+};
+
+/* ------------------------------------------------------------------ */
+/* Prototypes for callbacks  */
+static void device_added_cb(SpiceUsbDeviceManager *manager,
+    SpiceUsbDevice *device, gpointer user_data);
+static void device_removed_cb(SpiceUsbDeviceManager *manager,
+    SpiceUsbDevice *device, gpointer user_data);
+static void device_changed_cb(SpiceUsbDeviceManager *manager,
+    SpiceUsbDevice *device, gpointer user_data);
+static void device_error_cb(SpiceUsbDeviceManager *manager,
+    SpiceUsbDevice *device, GError *err, gpointer user_data);
+static gboolean spice_usb_device_widget_update_status(gpointer user_data);
+
+/* ------------------------------------------------------------------ */
+/* gobject glue                                                       */
+
+#define SPICE_USB_DEVICE_WIDGET_GET_PRIVATE(obj) \
+    (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_USB_DEVICE_WIDGET, \
+                                 SpiceUsbDeviceWidgetPrivate))
+
+enum {
+    PROP_0,
+    PROP_SESSION,
+    PROP_DEVICE_FORMAT_STRING,
+};
+
+enum {
+    CONNECT_FAILED,
+    LAST_SIGNAL,
+};
+
+typedef struct {
+    GtkTreeView *tree_view;
+    GtkTreeStore *tree_store;
+} SpiceUsbDeviceWidgetTree;
+
+struct _SpiceUsbDeviceWidgetPrivate {
+    SpiceSession *session;
+    gchar *device_format_string;
+    SpiceUsbDeviceManager *manager;
+    GtkWidget *info_bar;
+    GtkWidget *label;
+    SpiceUsbDeviceWidgetTree cd_tree;
+    SpiceUsbDeviceWidgetTree usb_tree;
+    GdkPixbuf *icon_cd;
+    GdkPixbuf *icon_connected;
+    GdkPixbuf *icon_disconn;
+    GdkPixbuf *icon_warning;
+    GdkPixbuf *icon_info;
+    gchar *err_msg;
+    gsize device_count;
+};
+
+static guint signals[LAST_SIGNAL] = { 0, };
+
+G_DEFINE_TYPE(SpiceUsbDeviceWidget, spice_usb_device_widget, GTK_TYPE_BOX);
+
+/* TREE */
+
+enum column_id
+{
+    COL_REDIRECT = 0,
+    COL_ADDRESS,
+    COL_CONNECT_ICON,
+    COL_CD_ICON,
+    COL_VENDOR,
+    COL_PRODUCT,
+    COL_FILE,
+    COL_LOADED,
+    COL_LOCKED,
+    COL_IDLE,
+    /* internal columns */
+    COL_REVISION,
+    COL_CD_DEV,
+    COL_LUN_ITEM,
+    COL_DEV_ITEM,
+    COL_ITEM_DATA,
+    COL_CONNECTED,
+    COL_CAN_REDIRECT,
+    COL_ROW_COLOR,
+    COL_ROW_COLOR_SET,
+    NUM_COLS,
+
+    INVALID_COL
+};
+
+// there is a possibility to use different names
+// for columns in USB device list and CD device list, if needed
+// currently they are identical
+static const char *column_names_cd[NUM_COLS];
+static const char *column_names_usb[NUM_COLS];
+
+#define SET_COLUMN(n, s) column_names_cd[n] = column_names_usb[n] = s
+
+static void initialize_columns(void)
+{
+    SET_COLUMN(COL_REDIRECT, _("Redirect"));
+    SET_COLUMN(COL_ADDRESS, _("Address"));
+    SET_COLUMN(COL_CONNECT_ICON, _("Conn"));
+    SET_COLUMN(COL_CD_ICON, _("CD"));
+    SET_COLUMN(COL_VENDOR, _("Vendor"));
+    SET_COLUMN(COL_PRODUCT, _("Product"));
+    SET_COLUMN(COL_FILE, _("File/Device Path"));
+    SET_COLUMN(COL_LOADED, _("Loaded"));
+    SET_COLUMN(COL_LOCKED, _("Locked"));
+    SET_COLUMN(COL_IDLE, _("Idle"));
+    SET_COLUMN(COL_REVISION, "?Revision");
+    SET_COLUMN(COL_CD_DEV, "?CD_DEV");
+    SET_COLUMN(COL_LUN_ITEM, "?LUN_ITEM");
+    SET_COLUMN(COL_DEV_ITEM, "?DEV_ITEM");
+    SET_COLUMN(COL_ITEM_DATA, "?ITEM_DATA");
+    SET_COLUMN(COL_CONNECTED, "?CONNECTED");
+    SET_COLUMN(COL_CAN_REDIRECT, "?CAN_REDIRECT");
+    SET_COLUMN(COL_ROW_COLOR, "?ROW_COLOR");
+    SET_COLUMN(COL_ROW_COLOR_SET, "?ROW_COLOR_SET");
+};
+
+static const char *column_name(enum column_id id, gboolean is_cd)
+{
+    const char **col_name = is_cd ? column_names_cd : column_names_usb;
+    return col_name[id];
+}
+
+typedef struct _UsbWidgetLunItem {
+    SpiceUsbDeviceManager *manager;
+    SpiceUsbDevice *device;
+    guint lun;
+    SpiceUsbDeviceLunInfo info;
+} UsbWidgetLunItem;
+
+typedef struct _TreeFindUsbDev {
+    SpiceUsbDevice *usb_dev;
+    GtkTreeIter dev_iter;
+} TreeFindUsbDev;
+
+typedef void (*tree_item_toggled_cb)(GtkCellRendererToggle *, gchar *, gpointer);
+
+static void usb_widget_add_device(SpiceUsbDeviceWidget *self,
+                                          SpiceUsbDevice *usb_device,
+                                          GtkTreeIter *old_dev_iter);
+
+static gchar *usb_device_description(SpiceUsbDeviceManager *manager,
+                                     SpiceUsbDevice *device,
+                                     const gchar *format)
+{
+    SpiceUsbDeviceDescription desc;
+    gchar *descriptor;
+    gchar *res;
+    spice_usb_device_get_info(manager, device, &desc);
+    descriptor = g_strdup_printf("[%04x:%04x]", desc.vendor_id, desc.product_id);
+    if (!format) {
+        format = _("%s %s %s at %d-%d");
+    }
+    res = g_strdup_printf(format, desc.vendor, desc.product, descriptor, desc.bus, desc.address);
+    g_free(desc.vendor);
+    g_free(desc.product);
+    g_free(descriptor);
+    return res;
+}
+
+static GtkTreeStore* usb_widget_create_tree_store(void)
+{
+    GtkTreeStore *tree_store;
+
+    tree_store = gtk_tree_store_new(NUM_COLS,
+                        G_TYPE_BOOLEAN, /* COL_REDIRECT */
+                        G_TYPE_STRING, /* COL_ADDRESS */
+                        GDK_TYPE_PIXBUF, /* COL_CONNECT_ICON */
+                        GDK_TYPE_PIXBUF, /* COL_CD_ICON */
+                        G_TYPE_STRING, /* COL_VENDOR */
+                        G_TYPE_STRING, /* COL_PRODUCT */
+                        G_TYPE_STRING, /* COL_FILE */
+                        G_TYPE_BOOLEAN, /* COL_LOADED */
+                        G_TYPE_BOOLEAN, /* COL_LOCKED */
+                        G_TYPE_BOOLEAN, /* COL_IDLE */
+                        /* internal columns */
+                        G_TYPE_STRING, /* COL_REVISION */
+                        G_TYPE_BOOLEAN, /* COL_CD_DEV */
+                        G_TYPE_BOOLEAN, /* COL_LUN_ITEM */
+                        G_TYPE_BOOLEAN, /* COL_DEV_ITEM */
+                        G_TYPE_POINTER, /* COL_ITEM_DATA */
+                        G_TYPE_BOOLEAN, /* COL_CONNECTED */
+                        G_TYPE_BOOLEAN, /* COL_CAN_REDIRECT */
+                        G_TYPE_STRING, /* COL_ROW_COLOR */
+                        G_TYPE_BOOLEAN /* COL_ROW_COLOR_SET */ );
+    SPICE_DEBUG("tree store created");
+
+    return tree_store;
+}
+
+static GdkPixbuf *get_named_icon(const gchar *name, gint size)
+{
+    GtkIconInfo *info = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(), name, size, 0);
+    GdkPixbuf *pixbuf = gtk_icon_info_load_icon(info, NULL);
+    g_object_unref (info);
+    return pixbuf;
+}
+
+static void select_widget_size(GtkWidget *wg)
+{
+    GdkDisplay *d = gtk_widget_get_display(wg);
+    int i, w = 2000, h = 1024;
+    int n = gdk_display_get_n_monitors(d);
+    for (i = 0; i < n; ++i)
+    {
+        GdkMonitor *m = gdk_display_get_monitor(d, i);
+        if (m) {
+            GdkRectangle area;
+            gdk_monitor_get_workarea(m, &area);
+            SPICE_DEBUG("monitor %d: %d x %d @( %d, %d)",
+                i, area.width, area.height, area.x, area.y );
+            w = MIN(w, area.width);
+            h = MIN(h, area.height);
+        }
+    }
+
+    w = (w * 3) / 4;
+    h = h / 2;
+
+    SPICE_DEBUG("sizing widget as %d x %d", w, h);
+    gtk_widget_set_size_request(wg, w, h);
+}
+
+static void usb_widget_add_device(SpiceUsbDeviceWidget *self,
+                                          SpiceUsbDevice *usb_device,
+                                          GtkTreeIter *old_dev_iter)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    SpiceUsbDeviceManager *usb_dev_mgr = priv->manager;
+    SpiceUsbDeviceWidgetTree *tree;
+    GtkTreeView *tree_view;
+    GtkTreeStore *tree_store;
+    GtkTreeIter new_dev_iter;
+    SpiceUsbDeviceDescription dev_info;
+    GtkTreePath *new_dev_path;
+    gboolean is_dev_redirected, is_dev_connected, is_dev_cd;
+    gchar *addr_str;
+    GArray *lun_array;
+    guint lun_index;
+    GError *error = NULL;
+
+    is_dev_cd = spice_usb_device_manager_is_device_cd(usb_dev_mgr, usb_device);
+    tree = is_dev_cd ? &priv->cd_tree : &priv->usb_tree;
+    tree_view = tree->tree_view;
+    tree_store = tree->tree_store;
+
+    if (old_dev_iter == NULL) {
+        gtk_tree_store_append(tree_store, &new_dev_iter, NULL);
+    } else {
+        gtk_tree_store_insert_after(tree_store, &new_dev_iter, NULL, old_dev_iter);
+        gtk_tree_store_remove(tree_store, old_dev_iter);
+    }
+
+    spice_usb_device_get_info(usb_dev_mgr, usb_device, &dev_info);
+    addr_str = g_strdup_printf("%d:%d", (gint)dev_info.bus, (gint)dev_info.address);
+    is_dev_connected = spice_usb_device_manager_is_device_connected(usb_dev_mgr, usb_device);
+    is_dev_redirected = is_dev_connected;
+    SPICE_DEBUG("usb device a:[%s] p:[%s] m:[%s] conn:%d cd:%d",
+        addr_str, dev_info.vendor, dev_info.product, is_dev_connected, is_dev_cd);
+
+    gtk_tree_store_set(tree_store, &new_dev_iter,
+        COL_REDIRECT, is_dev_redirected,
+        COL_ADDRESS, addr_str,
+        COL_CONNECT_ICON, is_dev_connected ? priv->icon_connected : priv->icon_disconn,
+        COL_CD_ICON, priv->icon_cd,
+        COL_VENDOR, dev_info.vendor,
+        COL_PRODUCT, dev_info.product,
+        COL_CD_DEV, is_dev_cd,
+        COL_LUN_ITEM, FALSE, /* USB device item */
+        COL_DEV_ITEM, TRUE, /* USB device item */
+        COL_ITEM_DATA, (gpointer)usb_device,
+        COL_CONNECTED, is_dev_connected,
+        COL_CAN_REDIRECT, spice_usb_device_manager_can_redirect_device(usb_dev_mgr, usb_device, &error),
+        COL_ROW_COLOR, "beige",
+        COL_ROW_COLOR_SET, TRUE,
+        -1);
+    g_clear_error(&error);
+
+    priv->device_count++;
+
+    /* get all the luns */
+    lun_array = spice_usb_device_manager_get_device_luns(usb_dev_mgr, usb_device);
+    for (lun_index = 0; lun_index < lun_array->len; lun_index++) {
+        UsbWidgetLunItem *lun_item;
+        GtkTreeIter lun_iter;
+        gchar lun_str[8];
+
+        lun_item = g_malloc(sizeof(*lun_item));
+        lun_item->manager = usb_dev_mgr;
+        lun_item->device = usb_device;
+        lun_item->lun = g_array_index(lun_array, guint, lun_index);
+        spice_usb_device_manager_device_lun_get_info(usb_dev_mgr, usb_device, lun_item->lun, &lun_item->info);
+        SPICE_DEBUG("lun %u v:[%s] p:[%s] r:[%s] file:[%s] lun_item:%p",
+                lun_index, lun_item->info.vendor, lun_item->info.product,
+                lun_item->info.revision, lun_item->info.file_path, lun_item);
+        g_snprintf(lun_str, 8, "↳%u", lun_item->lun);
+
+        /* Append LUN as a child of USB device */
+        gtk_tree_store_append(tree_store, &lun_iter, &new_dev_iter);
+        gtk_tree_store_set(tree_store, &lun_iter,
+                COL_ADDRESS, lun_str,
+                COL_VENDOR, lun_item->info.vendor,
+                COL_PRODUCT, lun_item->info.product,
+                COL_REVISION, lun_item->info.revision,
+                COL_FILE, lun_item->info.file_path,
+                COL_LOADED, lun_item->info.loaded,
+                COL_LOCKED, lun_item->info.locked,
+                COL_IDLE, !lun_item->info.started,
+                COL_CD_DEV, FALSE,
+                COL_LUN_ITEM, TRUE, /* LUN item */
+                COL_DEV_ITEM, FALSE, /* LUN item */
+                COL_ITEM_DATA, (gpointer)lun_item,
+                COL_CONNECTED, is_dev_connected,
+                COL_ROW_COLOR, "azure",
+                COL_ROW_COLOR_SET, TRUE,
+                -1);
+    }
+    g_array_unref(lun_array);
+
+    new_dev_path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree_store), &new_dev_iter);
+    gtk_tree_view_expand_row(tree_view, new_dev_path, FALSE);
+    gtk_tree_path_free(new_dev_path);
+
+    g_free(dev_info.vendor);
+    g_free(dev_info.product);
+    g_free(addr_str);
+}
+
+static gboolean tree_item_is_lun(GtkTreeStore *tree_store, GtkTreeIter *iter)
+{
+    gboolean is_lun;
+    gtk_tree_model_get(GTK_TREE_MODEL(tree_store), iter, COL_LUN_ITEM, &is_lun, -1);
+    return is_lun;
+}
+
+static gboolean usb_widget_tree_store_find_usb_dev_foreach_cb(GtkTreeModel *tree_model,
+                                                              GtkTreePath *path, GtkTreeIter *iter,
+                                                              gpointer user_data)
+{
+    TreeFindUsbDev *find_info = (TreeFindUsbDev *)user_data;
+    SpiceUsbDevice *find_usb_device = find_info->usb_dev;
+    SpiceUsbDevice *usb_device;
+    gboolean is_lun_item;
+
+    gtk_tree_model_get(tree_model, iter,
+                       COL_LUN_ITEM, &is_lun_item,
+                       COL_ITEM_DATA, (gpointer *)&usb_device,
+                       -1);
+    if (!is_lun_item && usb_device == find_usb_device) {
+        find_info->dev_iter = *iter;
+        find_info->usb_dev  = NULL;
+        SPICE_DEBUG("Usb dev found %p iter %p", usb_device, iter);
+        return TRUE; /* stop iterating */
+    } else {
+        return FALSE; /* continue iterating */
+    }
+}
+
+static GtkTreeIter *usb_widget_tree_store_find_usb_device(GtkTreeStore *tree_store,
+                                                          SpiceUsbDevice *usb_device)
+{
+    TreeFindUsbDev find_info = { .usb_dev = usb_device,.dev_iter = {} };
+    GtkTreeIter *iter = NULL;
+
+    gtk_tree_model_foreach(GTK_TREE_MODEL(tree_store),
+                           usb_widget_tree_store_find_usb_dev_foreach_cb, (gpointer)&find_info);
+    // the callback sets 'usb_dev' field to zero if it finds the device
+    if (!find_info.usb_dev) {
+        iter = g_malloc(sizeof(*iter));
+        *iter = find_info.dev_iter;
+    }
+    return iter;
+}
+
+static gboolean usb_widget_remove_device(SpiceUsbDeviceWidget *self,
+                                         SpiceUsbDevice *usb_device)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeIter *old_dev_iter;
+    GtkTreeStore *tree_store = priv->usb_tree.tree_store;
+    // on WIN32 it is possible the backend device is already removed
+    // from the USB device manager list and we can't know it was
+    // USB or CD, do we will try both lists
+    old_dev_iter = usb_widget_tree_store_find_usb_device(tree_store, usb_device);
+    if (old_dev_iter != NULL) {
+        SPICE_DEBUG("USB Device removed");
+        gtk_tree_store_remove(tree_store, old_dev_iter);
+        priv->device_count--;
+        g_free(old_dev_iter);
+        return TRUE;
+    }
+    tree_store = priv->cd_tree.tree_store;
+    if (tree_store) {
+        old_dev_iter = usb_widget_tree_store_find_usb_device(tree_store, usb_device);
+    }
+    if (old_dev_iter != NULL) {
+        SPICE_DEBUG("CD Device removed");
+        gtk_tree_store_remove(tree_store, old_dev_iter);
+        priv->device_count--;
+        g_free(old_dev_iter);
+        return TRUE;
+    }
+    SPICE_DEBUG("Device %p not found!", usb_device);
+    return FALSE;
+}
+
+static GtkTreeViewColumn* view_add_toggle_column(SpiceUsbDeviceWidget *self,
+                                                 enum column_id toggle_col_id,
+                                                 enum column_id visible_col_id,
+                                                 enum column_id sensitive_col_id,
+                                                 tree_item_toggled_cb toggled_cb,
+                                                 gboolean is_cd)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkCellRenderer     *renderer;
+    GtkTreeViewColumn   *view_col;
+    const char *col_name = column_name(toggle_col_id, is_cd);
+
+    renderer = gtk_cell_renderer_toggle_new();
+
+    if (sensitive_col_id != INVALID_COL) {
+        view_col = gtk_tree_view_column_new_with_attributes(
+                        col_name,
+                        renderer,
+                        "active", toggle_col_id,
+                        "visible", visible_col_id,
+                        "activatable", sensitive_col_id,
+                        NULL);
+    } else {
+        view_col = gtk_tree_view_column_new_with_attributes(
+                        col_name,
+                        renderer,
+                        "active", toggle_col_id,
+                        "visible", visible_col_id,
+                        NULL);
+    }
+
+    gtk_tree_view_column_set_sizing(view_col, GTK_TREE_VIEW_COLUMN_FIXED);
+    gtk_tree_view_column_set_expand(view_col, FALSE);
+    gtk_tree_view_column_set_resizable(view_col, FALSE);
+    gtk_tree_view_append_column(is_cd ? priv->cd_tree.tree_view : priv->usb_tree.tree_view, view_col);
+
+    g_signal_connect(renderer, "toggled", G_CALLBACK(toggled_cb), self);
+
+    SPICE_DEBUG("view added toggle column [%u : %s] visible when [%u : %s]",
+            toggle_col_id, col_name,
+            visible_col_id, column_name(visible_col_id, is_cd));
+    return view_col;
+}
+
+static GtkTreeViewColumn* view_add_read_only_toggle_column(SpiceUsbDeviceWidget *self,
+                                                           enum column_id toggle_col_id,
+                                                           enum column_id visible_col_id,
+                                                           gboolean is_cd)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkCellRenderer     *renderer;
+    GtkTreeViewColumn   *view_col;
+    const char *col_name = column_name(toggle_col_id, is_cd);
+
+    renderer = gtk_cell_renderer_toggle_new();
+
+    view_col = gtk_tree_view_column_new_with_attributes(
+                    col_name,
+                    renderer,
+                    "active", toggle_col_id,
+                    "visible", visible_col_id,
+                    NULL);
+
+    gtk_cell_renderer_toggle_set_activatable(GTK_CELL_RENDERER_TOGGLE(renderer), FALSE);
+
+    gtk_tree_view_column_set_sizing(view_col, GTK_TREE_VIEW_COLUMN_FIXED);
+    gtk_tree_view_column_set_expand(view_col, FALSE);
+    gtk_tree_view_column_set_resizable(view_col, FALSE);
+    gtk_tree_view_append_column(is_cd ? priv->cd_tree.tree_view : priv->usb_tree.tree_view, view_col);
+
+    SPICE_DEBUG("view added read-only toggle column [%u : %s] visible when [%u : %s]",
+            toggle_col_id, col_name,
+            visible_col_id, column_name(visible_col_id, is_cd));
+    return view_col;
+}
+
+static GtkTreeViewColumn* view_add_text_column(SpiceUsbDeviceWidget *self,
+                                               enum column_id col_id,
+                                               gboolean expandable,
+                                               gboolean is_cd)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkCellRenderer     *renderer;
+    GtkTreeViewColumn   *view_col;
+
+    renderer = gtk_cell_renderer_text_new();
+
+    view_col = gtk_tree_view_column_new_with_attributes(
+                    column_name(col_id, is_cd),
+                    renderer,
+                    "text", col_id,
+                    //"cell-background", COL_ROW_COLOR,
+                    //"cell-background-set", COL_ROW_COLOR_SET,
+                    NULL);
+
+    gtk_tree_view_column_set_sizing(view_col, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
+    gtk_tree_view_column_set_resizable(view_col, TRUE);
+    gtk_tree_view_column_set_expand(view_col, expandable);
+
+    gtk_tree_view_append_column(is_cd ? priv->cd_tree.tree_view : priv->usb_tree.tree_view, view_col);
+
+    SPICE_DEBUG("view added text column [%u : %s]", col_id, column_name(col_id, is_cd));
+    return view_col;
+}
+
+static GtkTreeViewColumn* view_add_pixbuf_column(SpiceUsbDeviceWidget *self,
+                                                 enum column_id col_id,
+                                                 enum column_id visible_col_id,
+                                                 gboolean is_cd)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkCellRenderer     *renderer;
+    GtkTreeViewColumn   *view_col;
+    const char *col_name = column_name(col_id, is_cd);
+
+    renderer = gtk_cell_renderer_pixbuf_new();
+
+    if (visible_col_id == INVALID_COL) {
+        view_col = gtk_tree_view_column_new_with_attributes(
+                        col_name,
+                        renderer,
+                        "pixbuf", col_id,
+                        NULL);
+        SPICE_DEBUG("view added pixbuf col[%u : %s] visible always", col_id, col_name);
+    } else {
+        view_col = gtk_tree_view_column_new_with_attributes(
+                        col_name,
+                        renderer,
+                        "pixbuf", col_id,
+                        "visible", visible_col_id,
+                        NULL);
+        SPICE_DEBUG("view added pixbuf col[%u : %s] visible when[%u : %s]",
+                col_id, col_name, visible_col_id, column_name(visible_col_id, is_cd));
+    }
+    gtk_tree_view_append_column(is_cd ? priv->cd_tree.tree_view : priv->usb_tree.tree_view, view_col);
+    return view_col;
+}
+
+/* Toggle handlers */
+
+static gboolean tree_item_toggle_get_val(GtkTreeStore *tree_store, gchar *path_str, GtkTreeIter *iter, enum column_id col_id)
+{
+    gboolean toggle_val;
+
+    gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(tree_store), iter, path_str);
+    gtk_tree_model_get(GTK_TREE_MODEL(tree_store), iter, col_id, &toggle_val, -1);
+
+    return toggle_val;
+}
+
+static void tree_item_toggle_set(GtkTreeStore *tree_store, GtkTreeIter *iter, enum column_id col_id, gboolean new_val)
+{
+    gtk_tree_store_set(tree_store, iter, col_id, new_val, -1);
+}
+
+typedef struct _connect_cb_data {
+    SpiceUsbDeviceWidget *self;
+    SpiceUsbDevice *usb_dev;
+} connect_cb_data;
+
+static void connect_cb_data_free(connect_cb_data *user_data)
+{
+    spice_usb_device_widget_update_status(user_data->self);
+    g_object_unref(user_data->self);
+    g_boxed_free(spice_usb_device_get_type(), user_data->usb_dev);
+    g_free(user_data);
+}
+
+static void usb_widget_connect_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    connect_cb_data *cb_data = user_data;
+    SpiceUsbDeviceWidget *self = cb_data->self;
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    SpiceUsbDevice *usb_dev = cb_data->usb_dev;
+    GError *err = NULL;
+    GtkTreeIter *dev_iter;
+    gchar *desc;
+    gboolean finished;
+    gboolean is_cd = spice_usb_device_manager_is_device_cd(priv->manager, usb_dev);
+    GtkTreeStore *tree_store = is_cd ? priv->cd_tree.tree_store : priv->usb_tree.tree_store;
+
+    dev_iter = usb_widget_tree_store_find_usb_device(tree_store, usb_dev);
+    if (!dev_iter) {
+        return;
+    }
+
+    desc = usb_device_description(priv->manager, usb_dev, priv->device_format_string);
+    SPICE_DEBUG("Connect cb: %p %s", usb_dev, desc);
+
+    finished = spice_usb_device_manager_connect_device_finish(priv->manager, res, &err);
+    if (finished) {
+        gtk_tree_store_set(tree_store, dev_iter,
+                           COL_CONNECT_ICON, priv->icon_connected,
+                           COL_CONNECTED, TRUE,
+                           -1);
+    } else {
+        gtk_tree_store_set(tree_store, dev_iter,
+                           COL_REDIRECT, FALSE,
+                           -1);
+        g_prefix_error(&err, "Device connect failed %s: ", desc);
+        if (err) {
+            SPICE_DEBUG("%s", err->message);
+            g_signal_emit(self, signals[CONNECT_FAILED], 0, usb_dev, err);
+            g_error_free(err);
+        } else {
+            g_signal_emit(self, signals[CONNECT_FAILED], 0, usb_dev, NULL);
+        }
+
+        /* don't trigger a disconnect if connect failed */
+        /*
+        g_signal_handlers_block_by_func(GTK_TOGGLE_BUTTON(user_data->check),
+                                        checkbox_clicked_cb, self);
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(user_data->check), FALSE);
+        g_signal_handlers_unblock_by_func(GTK_TOGGLE_BUTTON(user_data->check),
+                                        checkbox_clicked_cb, self);
+        */
+    }
+    g_free(desc);
+    g_free(dev_iter);
+    connect_cb_data_free(user_data);
+}
+
+static void usb_widget_disconnect_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    connect_cb_data *cb_data = user_data;
+    SpiceUsbDeviceWidget *self = cb_data->self;
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    SpiceUsbDevice *usb_dev = cb_data->usb_dev;
+    GError *err = NULL;
+    GtkTreeIter *dev_iter;
+    gchar *desc;
+    gboolean finished;
+    gboolean is_cd = spice_usb_device_manager_is_device_cd(priv->manager, usb_dev);
+    GtkTreeStore *tree_store = is_cd ? priv->cd_tree.tree_store : priv->usb_tree.tree_store;
+
+    dev_iter = usb_widget_tree_store_find_usb_device(tree_store, usb_dev);
+    if (!dev_iter) {
+        return;
+    }
+
+    desc = usb_device_description(priv->manager, usb_dev, priv->device_format_string);
+    SPICE_DEBUG("Disconnect cb: %p %s", usb_dev, desc);
+
+    finished = spice_usb_device_manager_disconnect_device_finish(priv->manager, res, &err);
+    if (finished) {
+        gtk_tree_store_set(tree_store, dev_iter,
+                           COL_CONNECT_ICON, priv->icon_disconn,
+                           COL_CONNECTED, FALSE,
+                           -1);
+    } else {
+        gtk_tree_store_set(tree_store, dev_iter,
+                           COL_REDIRECT, TRUE,
+                           -1);
+        g_prefix_error(&err, "Device disconnect failed %s: ", desc);
+        if (err) {
+            SPICE_DEBUG("%s", err->message);
+            g_error_free(err);
+        }
+    }
+    g_free(desc);
+    g_free(dev_iter);
+    connect_cb_data_free(user_data);
+}
+
+static void tree_item_toggled_cb_redirect(GtkCellRendererToggle *cell,
+                                          gchar *path_str,
+                                          SpiceUsbDeviceWidget *self,
+                                          GtkTreeStore *tree_store)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    connect_cb_data *cb_data = g_new(connect_cb_data, 1);
+    SpiceUsbDevice *usb_dev;
+    GtkTreeIter iter;
+    gboolean new_redirect_val;
+
+    new_redirect_val = !tree_item_toggle_get_val(tree_store, path_str, &iter, COL_REDIRECT);
+    SPICE_DEBUG("Redirect: %s", new_redirect_val ? "ON" : "OFF");
+    tree_item_toggle_set(tree_store, &iter, COL_REDIRECT, new_redirect_val);
+
+    gtk_tree_model_get(GTK_TREE_MODEL(tree_store), &iter, COL_ITEM_DATA, (gpointer *)&usb_dev, -1);
+    cb_data->self = g_object_ref(self);
+    cb_data->usb_dev = g_boxed_copy(spice_usb_device_get_type(), usb_dev);
+
+    if (new_redirect_val) {
+        spice_usb_device_manager_connect_device_async(priv->manager, usb_dev,
+                                                      NULL, /* cancellable */
+                                                      usb_widget_connect_cb, cb_data);
+    } else {
+        spice_usb_device_manager_disconnect_device_async(priv->manager, usb_dev,
+                                                         NULL, /* cancellable */
+                                                         usb_widget_disconnect_cb, cb_data);
+
+    }
+    spice_usb_device_widget_update_status(self);
+}
+
+static void tree_item_toggled_cb_redirect_cd(GtkCellRendererToggle *cell,
+                                             gchar *path_str,
+                                             gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    tree_item_toggled_cb_redirect(cell, path_str, self, priv->cd_tree.tree_store);
+}
+
+static void tree_item_toggled_cb_redirect_usb(GtkCellRendererToggle *cell,
+                                             gchar *path_str,
+                                             gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    tree_item_toggled_cb_redirect(cell, path_str, self, priv->usb_tree.tree_store);
+}
+
+/* Signal handlers */
+
+static void device_added_cb(SpiceUsbDeviceManager *usb_dev_mgr,
+    SpiceUsbDevice *usb_device, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+
+    SPICE_DEBUG("Signal: Device Added");
+
+    usb_widget_add_device(self, usb_device, NULL);
+
+    spice_usb_device_widget_update_status(self);
+}
+
+static void device_removed_cb(SpiceUsbDeviceManager *usb_dev_mgr,
+    SpiceUsbDevice *usb_device, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    gboolean dev_removed;
+
+    SPICE_DEBUG("Signal: Device Removed");
+
+    dev_removed = usb_widget_remove_device(self, usb_device);
+    if (dev_removed) {
+        spice_usb_device_widget_update_status(self);
+    }
+}
+
+static void device_changed_cb(SpiceUsbDeviceManager *usb_dev_mgr,
+    SpiceUsbDevice *usb_device, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeIter *old_dev_iter;
+    gboolean is_cd = spice_usb_device_manager_is_device_cd(priv->manager, usb_device);
+    GtkTreeStore *tree_store = is_cd ? priv->cd_tree.tree_store : priv->usb_tree.tree_store;
+
+    SPICE_DEBUG("Signal: Device Changed");
+
+    old_dev_iter = usb_widget_tree_store_find_usb_device(tree_store, usb_device);
+    if (old_dev_iter != NULL) {
+
+        usb_widget_add_device(self, usb_device, old_dev_iter);
+
+        spice_usb_device_widget_update_status(self);
+        g_free(old_dev_iter);
+    } else {
+        SPICE_DEBUG("Device not found!");
+    }
+}
+
+static void device_error_cb(SpiceUsbDeviceManager *manager,
+    SpiceUsbDevice *usb_device, GError *err, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeIter *dev_iter;
+    gboolean is_cd = spice_usb_device_manager_is_device_cd(priv->manager, usb_device);
+    GtkTreeStore *tree_store = is_cd ? priv->cd_tree.tree_store : priv->usb_tree.tree_store;
+
+    SPICE_DEBUG("Signal: Device Error");
+
+    dev_iter = usb_widget_tree_store_find_usb_device(tree_store, usb_device);
+    if (dev_iter != NULL) {
+        tree_item_toggle_set(tree_store, dev_iter, COL_REDIRECT, FALSE);
+        spice_usb_device_widget_update_status(self);
+        g_free(dev_iter);
+    } else {
+        SPICE_DEBUG("Device not found!");
+    }
+}
+
+/* Selection handler */
+
+static void tree_selection_changed_cb(GtkTreeSelection *select, gpointer user_data)
+{
+    GtkTreeModel *tree_model;
+    GtkTreeIter iter;
+    GtkTreePath *path;
+    gboolean is_lun;
+    UsbWidgetLunItem *lun_item;
+    gchar *txt[3];
+
+    if (gtk_tree_selection_get_selected(select, &tree_model, &iter)) {
+        gtk_tree_model_get(tree_model, &iter,
+                COL_VENDOR, &txt[0],
+                COL_PRODUCT, &txt[1],
+                COL_REVISION, &txt[2],
+                COL_LUN_ITEM, &is_lun,
+                COL_ITEM_DATA, (gpointer *)&lun_item,
+                -1);
+        path = gtk_tree_model_get_path(tree_model, &iter);
+
+        SPICE_DEBUG("selected: %s,%s,%s [%s %s] [%s]",
+                txt[0], txt[1],
+                is_lun ? txt[2] : "--",
+                is_lun ? "LUN" : "USB-DEV",
+                is_lun ? lun_item->info.file_path : "--",
+                gtk_tree_path_to_string(path));
+
+        if (txt[0]) {
+            g_free(txt[0]);
+        }
+        if (txt[1]) {
+            g_free(txt[1]);
+        }
+        if (txt[2]) {
+            g_free(txt[2]);
+        }
+        gtk_tree_path_free(path);
+    }
+}
+
+static GtkTreeSelection* set_selection_handler(GtkTreeView *tree_view)
+{
+    GtkTreeSelection *select;
+
+    select = gtk_tree_view_get_selection(tree_view);
+    gtk_tree_selection_set_mode(select, GTK_SELECTION_SINGLE);
+
+    g_signal_connect(G_OBJECT(select), "changed",
+                     G_CALLBACK(tree_selection_changed_cb),
+                     NULL);
+
+    SPICE_DEBUG("selection handler set");
+    return select;
+}
+
+static GtkWidget *create_image_button_box(const gchar *label_str, const gchar *icon_name, GtkWidget *parent)
+{
+    GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+    GtkWidget *icon = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU);
+    GtkWidget *label = gtk_accel_label_new(label_str);
+    GtkAccelGroup *accel_group = gtk_accel_group_new();
+    guint accel_key;
+
+    /* add icon */
+    gtk_container_add(GTK_CONTAINER(box), icon);
+
+    /* add label */
+    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
+    gtk_label_set_use_underline(GTK_LABEL(label), TRUE);
+    g_object_get(G_OBJECT(label), "mnemonic-keyval", &accel_key, NULL);
+    gtk_widget_add_accelerator(parent, "activate", accel_group, accel_key,
+                               GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
+    gtk_accel_label_set_accel_widget(GTK_ACCEL_LABEL(label), parent);
+    gtk_box_pack_end(GTK_BOX(box), label, TRUE, TRUE, 0);
+
+    /* add the new box to the parent widget */
+    gtk_container_add(GTK_CONTAINER(parent), box);
+
+    return box;
+}
+
+/* LUN properties dialog */
+
+typedef struct _lun_properties_dialog {
+    GtkWidget *dialog;
+    GtkWidget *advanced_grid;
+    gboolean advanced_shown;
+
+    GtkWidget *file_entry;
+    GtkWidget *vendor_entry;
+    GtkWidget *product_entry;
+    GtkWidget *revision_entry;
+    GtkWidget *loaded_switch;
+    GtkWidget *locked_switch;
+} lun_properties_dialog;
+
+#if 1
+static void usb_cd_choose_file(GtkWidget *button, gpointer user_data)
+{
+    GtkWidget *file_entry = (GtkWidget *)user_data;
+    GtkFileChooserNative *native;
+    GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
+    gint res;
+
+    native = gtk_file_chooser_native_new("Choose File for USB CD",
+        GTK_WINDOW(gtk_widget_get_toplevel(file_entry)),
+        action,
+        "_Open",
+        "_Cancel");
+
+    res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
+    if (res == GTK_RESPONSE_ACCEPT) {
+        char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
+        gtk_entry_set_alignment(GTK_ENTRY(file_entry), 1);
+        gtk_entry_set_text(GTK_ENTRY(file_entry), filename);
+        g_free(filename);
+    }
+    else {
+        gtk_widget_grab_focus(button);
+    }
+
+    g_object_unref(native);
+}
+#else
+// to be removed
+static void usb_cd_choose_file(GtkWidget *button, gpointer user_data)
+{
+    GtkWidget *file_entry = (GtkWidget *)user_data;
+    GtkWidget *dialog;
+    gint res;
+
+    dialog = gtk_file_chooser_dialog_new ("Choose File for USB CD",
+                                          GTK_WINDOW(gtk_widget_get_toplevel(file_entry)),
+                                          GTK_FILE_CHOOSER_ACTION_OPEN,
+                                          "_Cancel",
+                                          GTK_RESPONSE_CANCEL,
+                                          "_Ok",
+                                          GTK_RESPONSE_ACCEPT,
+                                          NULL);
+    gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
+
+    res = gtk_dialog_run(GTK_DIALOG(dialog));
+    if (res == GTK_RESPONSE_ACCEPT) {
+        char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+        gtk_entry_set_alignment(GTK_ENTRY(file_entry), 1);
+        gtk_entry_set_text(GTK_ENTRY(file_entry), filename);
+        g_free(filename);
+    }
+    gtk_widget_destroy(dialog);
+}
+#endif
+
+static gboolean lun_properties_dialog_loaded_switch_cb(GtkWidget *widget,
+                                                       gboolean state, gpointer user_data)
+{
+    lun_properties_dialog *lun_dialog = user_data;
+
+    gtk_widget_set_sensitive(lun_dialog->locked_switch, state);
+    gtk_widget_set_can_focus(lun_dialog->locked_switch, state);
+
+    return FALSE; /* call default signal handler */
+}
+
+static void lun_properties_dialog_toggle_advanced(GtkWidget *widget, gpointer user_data)
+{
+    lun_properties_dialog *lun_dialog = user_data;
+
+    if (lun_dialog->advanced_shown) {
+        gtk_widget_hide(lun_dialog->advanced_grid);
+        lun_dialog->advanced_shown = FALSE;
+    } else {
+        gtk_widget_show_all(lun_dialog->advanced_grid);
+        lun_dialog->advanced_shown = TRUE;
+    }
+}
+
+static void create_lun_properties_dialog(SpiceUsbDeviceWidget *self,
+                                         GtkWidget *parent_window,
+                                         SpiceUsbDeviceLunInfo *lun_info,
+                                         lun_properties_dialog *lun_dialog)
+{
+    // SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkWidget *dialog, *content_area;
+    GtkWidget *grid, *advanced_grid;
+    GtkWidget *file_entry, *choose_button;
+    GtkWidget *advanced_button, *advanced_icon;
+    GtkWidget *vendor_entry, *product_entry, *revision_entry;
+    GtkWidget *loaded_switch, *loaded_label;
+    GtkWidget *locked_switch, *locked_label;
+    gint nrow = 0;
+
+    dialog = gtk_dialog_new_with_buttons (!lun_info ? "Add CD LUN" : "CD LUN Settings",
+                    GTK_WINDOW(parent_window),
+                    GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, /* flags */
+                    !lun_info ? "Add" : "OK", GTK_RESPONSE_ACCEPT,
+                    "Cancel", GTK_RESPONSE_REJECT,
+                    NULL);
+
+    gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
+    gtk_container_set_border_width(GTK_CONTAINER(dialog), 12);
+    gtk_box_set_spacing(GTK_BOX(gtk_bin_get_child(GTK_BIN(dialog))), 12);
+
+    content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+
+    /* main grid - always visible */
+    grid = gtk_grid_new();
+    gtk_grid_set_row_spacing(GTK_GRID(grid), 12);
+    gtk_grid_set_column_homogeneous(GTK_GRID(grid), FALSE);
+    gtk_container_add(GTK_CONTAINER(content_area), grid);
+
+    /* File path label */
+    gtk_grid_attach(GTK_GRID(grid),
+            gtk_label_new("Select file or device"),
+            0, nrow++, // left top
+            7, 1); // width height
+
+    /* file/device path entry */
+    file_entry = gtk_entry_new();
+    gtk_widget_set_hexpand(file_entry, TRUE);
+    if (!lun_info) {
+        gtk_entry_set_placeholder_text(GTK_ENTRY(file_entry), "file-path");
+    } else {
+        gtk_entry_set_text(GTK_ENTRY(file_entry), lun_info->file_path);
+        if (lun_info->loaded) {
+            gtk_editable_set_editable(GTK_EDITABLE(file_entry), FALSE);
+            gtk_widget_set_can_focus(file_entry, FALSE);
+        }
+    }
+    gtk_grid_attach(GTK_GRID(grid),
+            file_entry,
+            0, nrow, // left top
+            6, 1); // width height
+
+    /* choose button */
+    choose_button = gtk_button_new_with_mnemonic("_Choose File");
+    gtk_widget_set_hexpand(choose_button, FALSE);
+    g_signal_connect(GTK_BUTTON(choose_button),
+                     "clicked", G_CALLBACK(usb_cd_choose_file), file_entry);
+    if (lun_info && lun_info->loaded) {
+        gtk_widget_set_sensitive(choose_button, FALSE);
+        gtk_widget_set_can_focus(choose_button, FALSE);
+    }
+
+    gtk_grid_attach(GTK_GRID(grid),
+            choose_button,
+            6, nrow++, // left top
+            1, 1); // width height
+
+    /* advanced button */
+    advanced_button = gtk_button_new_with_label("Advanced");
+    gtk_button_set_relief(GTK_BUTTON(advanced_button), GTK_RELIEF_NONE);
+    advanced_icon = gtk_image_new_from_icon_name("preferences-system", GTK_ICON_SIZE_BUTTON);
+    gtk_button_set_image(GTK_BUTTON(advanced_button), advanced_icon);
+    gtk_button_set_always_show_image(GTK_BUTTON(advanced_button), TRUE);
+    g_signal_connect(advanced_button, "clicked", G_CALLBACK(lun_properties_dialog_toggle_advanced), lun_dialog);
+
+    gtk_grid_attach(GTK_GRID(grid),
+            advanced_button,
+            0, nrow++, // left top
+            1, 1); // width height
+
+    /* advanced grid */
+    advanced_grid = gtk_grid_new();
+    gtk_grid_set_row_spacing(GTK_GRID(advanced_grid), 12);
+    gtk_grid_set_column_homogeneous(GTK_GRID(advanced_grid), FALSE);
+    gtk_container_add(GTK_CONTAINER(content_area), advanced_grid);
+
+    /* horizontal separator */
+    gtk_container_add(GTK_CONTAINER(content_area), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
+
+    /* pack advanced grid */
+    nrow = 0;
+
+    /* horizontal separator */
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            gtk_separator_new(GTK_ORIENTATION_HORIZONTAL),
+            0, nrow++, // left top
+            7, 1); // width height
+
+    /* product id labels */
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            gtk_label_new("Vendor"),
+            0, nrow, // left top
+            2, 1); // width height
+
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            gtk_label_new("Product"),
+            2, nrow, // left top
+            4, 1); // width height
+
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            gtk_label_new("Revision"),
+            6, nrow++, // left top
+            1, 1); // width height
+
+    /* vendor entry */
+    vendor_entry = gtk_entry_new();
+    gtk_entry_set_max_length(GTK_ENTRY(vendor_entry), 8);
+    if (lun_info) {
+        gtk_widget_set_sensitive(vendor_entry, FALSE);
+        gtk_widget_set_can_focus(vendor_entry, FALSE);
+        gtk_entry_set_text(GTK_ENTRY(vendor_entry), lun_info->vendor);
+    } else {
+        gtk_entry_set_placeholder_text(GTK_ENTRY(vendor_entry), "auto");
+    }
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            vendor_entry,
+            0, nrow, // left top
+            2, 1); // width height
+
+    /* product entry */
+    product_entry = gtk_entry_new();
+    gtk_entry_set_max_length(GTK_ENTRY(product_entry), 16);
+    if (lun_info) {
+        gtk_widget_set_sensitive(product_entry, FALSE);
+        gtk_widget_set_can_focus(product_entry, FALSE);
+        gtk_entry_set_text(GTK_ENTRY(product_entry), lun_info->product);
+    } else {
+        gtk_entry_set_placeholder_text(GTK_ENTRY(product_entry), "auto");
+    }
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            product_entry,
+            2, nrow, // left top
+            4, 1); // width height
+
+    /* revision entry */
+    revision_entry = gtk_entry_new();
+    gtk_entry_set_max_length(GTK_ENTRY(revision_entry), 4);
+    if (lun_info) {
+        gtk_widget_set_sensitive(revision_entry, FALSE);
+        gtk_widget_set_can_focus(revision_entry, FALSE);
+        gtk_entry_set_text(GTK_ENTRY(revision_entry), lun_info->revision);
+    } else {
+        gtk_entry_set_placeholder_text(GTK_ENTRY(revision_entry), "auto");
+    }
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            revision_entry,
+            6, nrow++, // left top
+            1, 1); // width height
+
+    /* horizontal separator */
+    if (!lun_info) {
+        gtk_grid_attach(GTK_GRID(advanced_grid),
+                gtk_separator_new(GTK_ORIENTATION_HORIZONTAL),
+                0, nrow++, // left top
+                7, 1); // width height
+    }
+
+    /* initially loaded switch */
+    loaded_label = gtk_label_new("Initially loaded:");
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+        loaded_label,
+        0, nrow, // left top
+        2, 1); // width height
+
+    loaded_switch = gtk_switch_new();
+    gtk_switch_set_state(GTK_SWITCH(loaded_switch), TRUE);
+    if (lun_info) {
+        gtk_widget_set_child_visible(loaded_switch, FALSE);
+        gtk_widget_set_child_visible(loaded_label, FALSE);
+    } else {
+        g_signal_connect(loaded_switch, "state-set",
+                         G_CALLBACK(lun_properties_dialog_loaded_switch_cb), lun_dialog);
+    }
+    gtk_widget_set_halign(loaded_switch, GTK_ALIGN_START);
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            loaded_switch,
+            2, nrow++, // left top
+            1, 1); // width height
+
+    /* initially locked switch */
+    locked_label = gtk_label_new("Initially locked:");
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+        locked_label,
+        0, nrow, // left top
+        2, 1); // width height
+
+    locked_switch = gtk_switch_new();
+    gtk_switch_set_state(GTK_SWITCH(locked_switch), FALSE);
+    gtk_widget_set_hexpand(locked_switch, FALSE);
+    if (lun_info) {
+        gtk_widget_set_child_visible(locked_switch, FALSE);
+        gtk_widget_set_child_visible(locked_label, FALSE);
+    }
+    gtk_widget_set_halign(locked_switch, GTK_ALIGN_START);
+    gtk_grid_attach(GTK_GRID(advanced_grid),
+            locked_switch,
+            2, nrow++, // left top
+            1, 1); // width height
+
+    lun_dialog->dialog = dialog;
+    lun_dialog->advanced_grid = advanced_grid;
+    lun_dialog->advanced_shown = FALSE;
+    lun_dialog->file_entry = file_entry;
+    lun_dialog->vendor_entry = vendor_entry;
+    lun_dialog->product_entry = product_entry;
+    lun_dialog->revision_entry = revision_entry;
+    lun_dialog->loaded_switch = loaded_switch;
+    lun_dialog->locked_switch = locked_switch;
+
+    gtk_widget_show_all(dialog);
+    gtk_widget_hide(advanced_grid);
+}
+
+static void lun_properties_dialog_get_info(lun_properties_dialog *lun_dialog,
+                                            SpiceUsbDeviceLunInfo *lun_info)
+{
+    lun_info->file_path = gtk_entry_get_text(GTK_ENTRY(lun_dialog->file_entry));
+    lun_info->vendor = gtk_entry_get_text(GTK_ENTRY(lun_dialog->vendor_entry));
+    lun_info->product = gtk_entry_get_text(GTK_ENTRY(lun_dialog->product_entry));
+    lun_info->revision = gtk_entry_get_text(GTK_ENTRY(lun_dialog->revision_entry));
+    lun_info->loaded = gtk_switch_get_active(GTK_SWITCH(lun_dialog->loaded_switch));
+    lun_info->locked = gtk_switch_get_active(GTK_SWITCH(lun_dialog->locked_switch));
+}
+
+/* Popup menu */
+static void view_popup_menu_on_eject(GtkWidget *menuitem, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeSelection *select = gtk_tree_view_get_selection(priv->cd_tree.tree_view);
+    GtkTreeModel *tree_model;
+    GtkTreeIter iter;
+
+    if (gtk_tree_selection_get_selected(select, &tree_model, &iter)) {
+        if (!tree_item_is_lun(priv->cd_tree.tree_store, &iter)) {
+            SpiceUsbDevice *usb_device;
+            gtk_tree_model_get(tree_model, &iter, COL_ITEM_DATA, (gpointer *)&usb_device, -1);
+            SPICE_DEBUG("%s - not applicable for USB device", __FUNCTION__);
+        }
+        else {
+            UsbWidgetLunItem *lun_item;
+            gtk_tree_model_get(tree_model, &iter, COL_ITEM_DATA, (gpointer *)&lun_item, -1);
+            spice_usb_device_manager_device_lun_load(
+                lun_item->manager, lun_item->device, lun_item->lun, !lun_item->info.loaded);
+        }
+    }
+    else {
+        SPICE_DEBUG("%s - failed to get selection", __FUNCTION__);
+    }
+}
+
+static void view_popup_menu_on_lock(GtkWidget *menuitem, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeSelection *select = gtk_tree_view_get_selection(priv->cd_tree.tree_view);
+    GtkTreeModel *tree_model;
+    GtkTreeIter iter;
+
+    if (gtk_tree_selection_get_selected(select, &tree_model, &iter)) {
+        if (!tree_item_is_lun(priv->cd_tree.tree_store, &iter)) {
+            SpiceUsbDevice *usb_device;
+            gtk_tree_model_get(tree_model, &iter, COL_ITEM_DATA, (gpointer *)&usb_device, -1);
+            SPICE_DEBUG("%s - not applicable for USB device", __FUNCTION__);
+        }
+        else {
+            UsbWidgetLunItem *lun_item;
+            gtk_tree_model_get(tree_model, &iter, COL_ITEM_DATA, (gpointer *)&lun_item, -1);
+            spice_usb_device_manager_device_lun_lock(
+                lun_item->manager, lun_item->device, lun_item->lun, !lun_item->info.locked);
+        }
+    }
+    else {
+        SPICE_DEBUG("%s - failed to get selection", __FUNCTION__);
+    }
+}
+
+static void view_popup_menu_on_remove(GtkWidget *menuitem, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeSelection *select = gtk_tree_view_get_selection(priv->cd_tree.tree_view);
+    GtkTreeModel *tree_model;
+    GtkTreeIter iter;
+
+    if (gtk_tree_selection_get_selected(select, &tree_model, &iter)) {
+        if (!tree_item_is_lun(priv->cd_tree.tree_store, &iter)) {
+            SpiceUsbDevice *usb_device;
+            gtk_tree_model_get(tree_model, &iter, COL_ITEM_DATA, (gpointer *)&usb_device, -1);
+            SPICE_DEBUG("Remove USB device");
+        } else {
+            UsbWidgetLunItem *lun_item;
+            gtk_tree_model_get(tree_model, &iter, COL_ITEM_DATA, (gpointer *)&lun_item, -1);
+            gtk_tree_selection_unselect_all(select);
+            spice_usb_device_manager_device_lun_remove(lun_item->manager, lun_item->device, lun_item->lun);
+        }
+    } else {
+        SPICE_DEBUG("Remove - failed to get selection");
+    }
+}
+
+static void view_popup_menu_on_settings(GtkWidget *menuitem, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeSelection *select = gtk_tree_view_get_selection(priv->cd_tree.tree_view);
+    GtkTreeModel *tree_model;
+    GtkTreeIter iter;
+
+    if (gtk_tree_selection_get_selected(select, &tree_model, &iter)) {
+        if (!tree_item_is_lun(priv->cd_tree.tree_store, &iter)) {
+            SPICE_DEBUG("No settings for USB device yet");
+        } else {
+            lun_properties_dialog lun_dialog;
+            UsbWidgetLunItem *lun_item;
+            gint resp;
+
+            gtk_tree_model_get(tree_model, &iter, COL_ITEM_DATA, (gpointer *)&lun_item, -1);
+            gtk_tree_selection_unselect_all(select);
+            create_lun_properties_dialog(self, NULL, &lun_item->info, &lun_dialog);
+
+            resp = gtk_dialog_run(GTK_DIALOG(lun_dialog.dialog));
+            if (resp == GTK_RESPONSE_ACCEPT) {
+                SpiceUsbDeviceLunInfo lun_info;
+                SPICE_DEBUG("response is ACCEPT");
+                lun_properties_dialog_get_info(&lun_dialog, &lun_info);
+                spice_usb_device_manager_device_lun_change_media(
+                    priv->manager, lun_item->device, lun_item->lun, &lun_info);
+            } else {
+                SPICE_DEBUG("response is REJECT");
+            }
+            gtk_widget_destroy(lun_dialog.dialog);
+        }
+    } else {
+        SPICE_DEBUG("Remove - failed to get selection");
+    }
+}
+
+static GtkWidget *view_popup_add_menu_item(GtkWidget *menu,
+    const gchar *label_str,
+    const gchar *icon_name,
+    GCallback cb_func, gpointer user_data)
+{
+    GtkWidget *menu_item = gtk_menu_item_new();
+    create_image_button_box(label_str, icon_name, menu_item);
+    g_signal_connect(menu_item, "activate", cb_func, user_data);
+
+    gtk_widget_show_all(menu_item);
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+    return menu_item;
+}
+
+static gboolean has_single_lun(SpiceUsbDeviceWidgetPrivate *priv, SpiceUsbDevice *device)
+{
+    gboolean result;
+    SpiceUsbDeviceManager *usb_dev_mgr = priv->manager;
+    GArray *lun_array;
+    lun_array = spice_usb_device_manager_get_device_luns(usb_dev_mgr, device);
+    result = lun_array && lun_array->len <= 1;
+    if (lun_array) {
+        g_array_unref(lun_array);
+    }
+    return result;
+}
+
+static void view_popup_menu(GtkTreeView *tree_view, GdkEventButton *event, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeSelection *select;
+    GtkTreeModel *tree_model;
+    GtkTreeIter iter;
+    UsbWidgetLunItem *lun_item;
+    gboolean is_loaded, is_locked;
+    GtkTreeIter *usb_dev_iter;
+    gboolean is_dev_connected;
+    GtkWidget *menu;
+
+    if (tree_view != priv->cd_tree.tree_view) {
+        SPICE_DEBUG("Not applicable for USB device");
+        return;
+    }
+
+    select = gtk_tree_view_get_selection(tree_view);
+
+    if (!gtk_tree_selection_get_selected(select, &tree_model, &iter)) {
+        SPICE_DEBUG("No tree view row is selected");
+        return;
+    }
+    if (!tree_item_is_lun(priv->cd_tree.tree_store, &iter)) {
+        SPICE_DEBUG("No settings for USB device yet");
+        return;
+    }
+
+    gtk_tree_model_get(tree_model, &iter,
+                       COL_ITEM_DATA, (gpointer *)&lun_item,
+                       COL_LOADED, &is_loaded,
+                       COL_LOCKED, &is_locked,
+                       -1);
+
+    usb_dev_iter = usb_widget_tree_store_find_usb_device(priv->cd_tree.tree_store, lun_item->device);
+    if (usb_dev_iter != NULL) {
+        gtk_tree_model_get(tree_model, usb_dev_iter,
+                           COL_CONNECTED, &is_dev_connected,
+                           -1);
+        g_free(usb_dev_iter);
+    } else {
+        is_dev_connected = FALSE;
+        SPICE_DEBUG("Failed to find USB device for LUN: %s|%s",
+                    lun_item->info.vendor, lun_item->info.product);
+    }
+    SPICE_DEBUG("Right-click on LUN: %s|%s, dev connected:%d, lun loaded:%d locked:%d",
+                lun_item->info.vendor, lun_item->info.product,
+                is_dev_connected, is_loaded, is_locked);
+
+    /* Set up the menu */
+    menu = gtk_menu_new();
+
+    view_popup_add_menu_item(menu, "_Settings", "preferences-system",
+                             G_CALLBACK(view_popup_menu_on_settings), user_data);
+    if (is_loaded) {
+        if (!is_locked) {
+            view_popup_add_menu_item(menu, "_Lock", "system-lock-screen",
+                                    G_CALLBACK(view_popup_menu_on_lock), user_data);
+            view_popup_add_menu_item(menu, "_Eject", "media-eject",
+                                     G_CALLBACK(view_popup_menu_on_eject), user_data);
+        } else {
+            view_popup_add_menu_item(menu, "_Unlock", "system-lock-screen",
+                                     G_CALLBACK(view_popup_menu_on_lock), user_data);
+        }
+    } else {
+        view_popup_add_menu_item(menu, "_Load", "media-eject",
+                                 G_CALLBACK(view_popup_menu_on_eject), user_data);
+    }
+
+    if (!is_dev_connected || has_single_lun(priv, lun_item->device)) {
+        view_popup_add_menu_item(menu, "_Remove", "edit-delete",
+                                 G_CALLBACK(view_popup_menu_on_remove), user_data);
+    }
+
+    gtk_widget_show_all(menu);
+    gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL);
+}
+
+static void treeview_select_current_row_by_pos(GtkTreeView *tree_view, gint x, gint y)
+{
+    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
+    if (gtk_tree_selection_count_selected_rows(selection) <= 1) {
+        GtkTreePath *path;
+        /* Get tree path for row that was clicked */
+        if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(tree_view), x, y, &path, NULL, NULL, NULL))
+        {
+            gtk_tree_selection_unselect_all(selection);
+            gtk_tree_selection_select_path(selection, path);
+            gtk_tree_path_free(path);
+        }
+    }
+}
+
+static gboolean treeview_on_right_button_pressed_cb(GtkWidget *view, GdkEventButton *event, gpointer user_data)
+{
+    GtkTreeView *tree_view = GTK_TREE_VIEW(view);
+    /* single click with the right mouse button */
+    if (event->type == GDK_BUTTON_PRESS  &&  event->button == 3) {
+        /* select the row that was clicked, it will also provide the context */
+        treeview_select_current_row_by_pos(tree_view, (gint)event->x, (gint)event->y);
+        view_popup_menu(tree_view, event, user_data);
+        return TRUE; /* we handled this */
+    } else {
+        return FALSE; /* we did not handle this */
+    }
+}
+
+static gboolean treeview_on_popup_key_pressed_cb(GtkWidget *view, gpointer user_data)
+{
+    view_popup_menu(GTK_TREE_VIEW(view), NULL, user_data);
+    return TRUE; /* we handled this */
+}
+
+/* Add LUN dialog */
+
+static void add_cd_lun_button_clicked_cb(GtkWidget *add_cd_button, gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkWidget *parent_window = gtk_widget_get_toplevel(add_cd_button);
+    lun_properties_dialog lun_dialog;
+    gint resp;
+
+    create_lun_properties_dialog(self, parent_window, NULL, &lun_dialog);
+
+    resp = gtk_dialog_run(GTK_DIALOG(lun_dialog.dialog));
+    if (resp == GTK_RESPONSE_ACCEPT) {
+        SpiceUsbDeviceLunInfo lun_info;
+        SPICE_DEBUG("response is ACCEPT");
+        lun_properties_dialog_get_info(&lun_dialog, &lun_info);
+        spice_usb_device_manager_add_cd_lun(priv->manager, &lun_info);
+    } else {
+        SPICE_DEBUG("response is REJECT");
+    }
+    gtk_widget_destroy(lun_dialog.dialog);
+}
+
+static void spice_usb_device_widget_get_property(GObject     *gobject,
+                                                 guint        prop_id,
+                                                 GValue      *value,
+                                                 GParamSpec  *pspec)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+    switch (prop_id) {
+    case PROP_SESSION:
+        g_value_set_object(value, priv->session);
+        break;
+    case PROP_DEVICE_FORMAT_STRING:
+        g_value_set_string(value, priv->device_format_string);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+        break;
+    }
+}
+
+static void spice_usb_device_widget_set_property(GObject       *gobject,
+                                                 guint          prop_id,
+                                                 const GValue  *value,
+                                                 GParamSpec    *pspec)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+    switch (prop_id) {
+    case PROP_SESSION:
+        priv->session = g_value_dup_object(value);
+        break;
+    case PROP_DEVICE_FORMAT_STRING:
+        priv->device_format_string = g_value_dup_string(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+        break;
+    }
+}
+
+static void spice_usb_device_widget_hide_info_bar(SpiceUsbDeviceWidget *self)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+    if (priv->info_bar) {
+        gtk_widget_destroy(priv->info_bar);
+        priv->info_bar = NULL;
+    }
+}
+
+static void
+spice_usb_device_widget_show_info_bar(SpiceUsbDeviceWidget *self,
+                                      const gchar          *message,
+                                      GtkMessageType        message_type,
+                                      GdkPixbuf            *icon_pixbuf)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkWidget *info_bar, *content_area, *hbox, *icon, *label;
+
+    spice_usb_device_widget_hide_info_bar(self);
+
+    info_bar = gtk_info_bar_new();
+    gtk_info_bar_set_message_type(GTK_INFO_BAR(info_bar), message_type);
+
+    content_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_bar));
+    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    gtk_container_add(GTK_CONTAINER(content_area), hbox);
+
+    icon = gtk_image_new_from_pixbuf(icon_pixbuf);
+    gtk_box_pack_start(GTK_BOX(hbox), icon, FALSE, FALSE, 10);
+
+    label = gtk_label_new(message);
+    gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
+
+    priv->info_bar = gtk_alignment_new(0.0, 0.0, 1.0, 0.0);
+    gtk_alignment_set_padding(GTK_ALIGNMENT(priv->info_bar), 0, 0, 0, 0);
+    gtk_container_add(GTK_CONTAINER(priv->info_bar), info_bar);
+
+    gtk_box_pack_start(GTK_BOX(self), priv->info_bar, FALSE, FALSE, 0);
+    gtk_box_reorder_child(GTK_BOX(self), priv->info_bar, 1); /* put after the lable */
+    gtk_widget_show_all(priv->info_bar);
+}
+
+static void spice_usb_device_widget_create_tree_view(SpiceUsbDeviceWidget *self, gboolean is_cd)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    GtkTreeStore *tree_store = usb_widget_create_tree_store();
+    GtkTreeView *tree_view = GTK_TREE_VIEW(gtk_tree_view_new());
+
+    if (is_cd) {
+        priv->cd_tree.tree_view = tree_view;
+        priv->cd_tree.tree_store = tree_store;
+    } else {
+        priv->usb_tree.tree_view = tree_view;
+        priv->usb_tree.tree_store = tree_store;
+    }
+
+    gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(tree_store));
+    g_object_unref(tree_store); /* destroy tree_store automatically with tree_view */
+
+    view_add_toggle_column(self, COL_REDIRECT, COL_DEV_ITEM, COL_CAN_REDIRECT,
+        is_cd ? tree_item_toggled_cb_redirect_cd : tree_item_toggled_cb_redirect_usb, is_cd);
+
+    view_add_text_column(self, COL_ADDRESS, FALSE, is_cd);
+
+    view_add_pixbuf_column(self, COL_CONNECT_ICON, COL_REDIRECT, is_cd);
+    if (is_cd) {
+        view_add_pixbuf_column(self, COL_CD_ICON, COL_CD_DEV, is_cd);
+    }
+
+    view_add_text_column(self, COL_VENDOR, TRUE, is_cd);
+    view_add_text_column(self, COL_PRODUCT, TRUE, is_cd);
+
+    if (is_cd) {
+        view_add_text_column(self, COL_FILE, TRUE, is_cd);
+        view_add_read_only_toggle_column(self, COL_LOADED, COL_LUN_ITEM, is_cd);
+        view_add_read_only_toggle_column(self, COL_LOCKED, COL_LUN_ITEM, is_cd);
+        // uncomment to show also 'idle' column for CD
+        //view_add_read_only_toggle_column(self, COL_IDLE, COL_LUN_ITEM, is_cd);
+    }
+
+    gtk_tree_selection_set_mode(
+            gtk_tree_view_get_selection(tree_view),
+            GTK_SELECTION_NONE);
+
+    if (is_cd) {
+        set_selection_handler(tree_view);
+    }
+}
+
+static void spice_usb_device_widget_signals_connect(SpiceUsbDeviceWidget *self)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+    g_signal_connect(priv->manager, "device-added",
+                     G_CALLBACK(device_added_cb), self);
+    g_signal_connect(priv->manager, "device-removed",
+                     G_CALLBACK(device_removed_cb), self);
+    g_signal_connect(priv->manager, "device-changed",
+                     G_CALLBACK(device_changed_cb), self);
+    // TODO: connect failed
+    g_signal_connect(priv->manager, "device-error",
+                     G_CALLBACK(device_error_cb), self);
+
+    g_signal_connect(priv->cd_tree.tree_view, "button-press-event",
+                     G_CALLBACK(treeview_on_right_button_pressed_cb), self);
+    g_signal_connect(priv->cd_tree.tree_view, "popup-menu",
+                     G_CALLBACK(treeview_on_popup_key_pressed_cb), self);
+}
+
+static void create_tree_window(SpiceUsbDeviceWidget *self, GtkTreeView *tree_view)
+{
+    GtkWidget *sw;
+    /* scrolled window */
+    sw = gtk_scrolled_window_new(NULL, NULL);
+    gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw),
+        GTK_SHADOW_ETCHED_IN);
+    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
+        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+    gtk_widget_set_hexpand(sw, TRUE);
+    gtk_widget_set_halign(sw, GTK_ALIGN_FILL);
+    gtk_widget_set_vexpand(sw, TRUE);
+    gtk_widget_set_valign(sw, GTK_ALIGN_FILL);
+    gtk_container_add(GTK_CONTAINER(sw), GTK_WIDGET(tree_view));
+    gtk_box_pack_start(GTK_BOX(self), sw, TRUE, TRUE, 0);
+}
+
+static void spice_usb_device_widget_constructed(GObject *gobject)
+{
+    SpiceUsbDeviceWidget *self;
+    GtkRequisition min_size, natural_size;
+    SpiceUsbDeviceWidgetPrivate *priv;
+    GtkWidget *hbox, *dev_label;
+    GtkWidget *add_cd_button, *add_cd_icon;
+    GPtrArray *devices = NULL;
+    GError *err = NULL;
+    gchar *str;
+    guint i;
+    gboolean cd_sharing_enabled = TRUE;
+    #ifndef USE_CD_SHARING
+    cd_sharing_enabled = FALSE;
+    #endif
+
+    self = SPICE_USB_DEVICE_WIDGET(gobject);
+    priv = self->priv;
+    if (!priv->session)
+        g_error("SpiceUsbDeviceWidget constructed without a session");
+
+    min_size.width = 600;
+    min_size.height = 300;
+    natural_size.width = 1200;
+    natural_size.height = 600;
+    gtk_widget_get_preferred_size(GTK_WIDGET(self), &min_size, &natural_size);
+
+    priv->label = gtk_label_new(NULL);
+    str = g_strdup_printf("<b>%s</b>", _("Select USB devices to redirect"));
+    gtk_label_set_markup(GTK_LABEL(priv->label), str);
+    g_free(str);
+    gtk_box_pack_start(GTK_BOX(self), priv->label, FALSE, FALSE, 0);
+
+    priv->icon_cd = get_named_icon("media-optical", GTK_ICON_SIZE_LARGE_TOOLBAR);
+    priv->icon_connected = get_named_icon("network-transmit-receive", GTK_ICON_SIZE_LARGE_TOOLBAR);
+    priv->icon_disconn = get_named_icon("network-offline", GTK_ICON_SIZE_LARGE_TOOLBAR);
+    priv->icon_warning = get_named_icon("dialog-warning", GTK_ICON_SIZE_LARGE_TOOLBAR);
+    priv->icon_info = get_named_icon("dialog-information", GTK_ICON_SIZE_LARGE_TOOLBAR);
+
+    priv->manager = spice_usb_device_manager_get(priv->session, &err);
+    if (err) {
+        spice_usb_device_widget_show_info_bar(self, err->message,
+                                              GTK_MESSAGE_WARNING, priv->icon_warning);
+        g_clear_error(&err);
+        return;
+    }
+
+    if (cd_sharing_enabled) {
+        spice_usb_device_widget_create_tree_view(self, TRUE);
+    } else {
+        priv->cd_tree.tree_store = NULL;
+        priv->cd_tree.tree_view = NULL;
+    }
+    spice_usb_device_widget_create_tree_view(self, FALSE);
+
+    spice_usb_device_widget_signals_connect(self);
+
+    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    gtk_box_pack_start(GTK_BOX(self), hbox, FALSE, FALSE, 0);
+
+    /* "Available devices" label - in hbox */
+    dev_label = gtk_label_new(_("Local USB devices"));
+    gtk_box_pack_start(GTK_BOX(hbox), dev_label, TRUE, FALSE, 0);
+    create_tree_window(self, priv->usb_tree.tree_view);
+
+    if (cd_sharing_enabled) {
+        hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+        gtk_box_pack_start(GTK_BOX(self), hbox, FALSE, FALSE, 0);
+        dev_label = gtk_label_new(_("Shared CD devices"));
+        gtk_box_pack_start(GTK_BOX(hbox), dev_label, TRUE, FALSE, 0);
+        /* "Add CD" button - in hbox */
+        add_cd_button = gtk_button_new_with_label(_("Add CD"));
+        gtk_button_set_always_show_image(GTK_BUTTON(add_cd_button), TRUE);
+        add_cd_icon = gtk_image_new_from_icon_name("list-add", GTK_ICON_SIZE_BUTTON);
+        gtk_button_set_image(GTK_BUTTON(add_cd_button), add_cd_icon);
+
+        gtk_widget_set_halign(add_cd_button, GTK_ALIGN_END);
+        g_signal_connect(add_cd_button, "clicked", G_CALLBACK(add_cd_lun_button_clicked_cb), self);
+        gtk_box_pack_start(GTK_BOX(hbox), add_cd_button, FALSE, FALSE, 0);
+        create_tree_window(self, priv->cd_tree.tree_view);
+    }
+    devices = spice_usb_device_manager_get_devices(priv->manager);
+    if (!devices)
+        goto end;
+
+    for (i = 0; i < devices->len; i++) {
+        SpiceUsbDevice *usb_device = g_ptr_array_index(devices, i);
+        usb_widget_add_device(self, usb_device, NULL);
+    }
+    g_ptr_array_unref(devices);
+
+    select_widget_size(GTK_WIDGET(self));
+
+end:
+    spice_usb_device_widget_update_status(self);
+}
+
+static void spice_usb_device_widget_finalize(GObject *object)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(object);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+    if (priv->manager) {
+        g_signal_handlers_disconnect_by_func(priv->manager,
+                                             device_added_cb, self);
+        g_signal_handlers_disconnect_by_func(priv->manager,
+                                             device_removed_cb, self);
+        g_signal_handlers_disconnect_by_func(priv->manager,
+                                             device_changed_cb, self);
+        g_signal_handlers_disconnect_by_func(priv->manager,
+                                             device_error_cb, self);
+    }
+    g_object_unref(priv->session);
+    g_free(priv->device_format_string);
+
+    if (G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize)
+        G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize(object);
+}
+
+static void spice_usb_device_widget_class_init(
+    SpiceUsbDeviceWidgetClass *klass)
+{
+    GObjectClass *gobject_class = (GObjectClass *)klass;
+    GParamSpec *pspec;
+
+    g_type_class_add_private (klass, sizeof (SpiceUsbDeviceWidgetPrivate));
+
+    gobject_class->constructed  = spice_usb_device_widget_constructed;
+    gobject_class->finalize     = spice_usb_device_widget_finalize;
+    gobject_class->get_property = spice_usb_device_widget_get_property;
+    gobject_class->set_property = spice_usb_device_widget_set_property;
+
+    /**
+     * SpiceUsbDeviceWidget:session:
+     *
+     * #SpiceSession this #SpiceUsbDeviceWidget is associated with
+     *
+     **/
+    pspec = g_param_spec_object("session",
+                                "Session",
+                                "SpiceSession",
+                                SPICE_TYPE_SESSION,
+                                G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+                                G_PARAM_STATIC_STRINGS);
+    g_object_class_install_property(gobject_class, PROP_SESSION, pspec);
+
+    /**
+     * SpiceUsbDeviceWidget:device-format-string:
+     *
+     * Format string to pass to spice_usb_device_get_description() for getting
+     * the device USB descriptions.
+     */
+    pspec = g_param_spec_string("device-format-string",
+                                "Device format string",
+                                "Format string for device description",
+                                NULL,
+                                G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+                                G_PARAM_STATIC_STRINGS);
+    g_object_class_install_property(gobject_class, PROP_DEVICE_FORMAT_STRING,
+                                    pspec);
+
+    /**
+     * SpiceUsbDeviceWidget::connect-failed:
+     * @widget: The #SpiceUsbDeviceWidget that emitted the signal
+     * @device: #SpiceUsbDevice boxed object corresponding to the added device
+     * @error:  #GError describing the reason why the connect failed
+     *
+     * The #SpiceUsbDeviceWidget::connect-failed signal is emitted whenever
+     * the user has requested for a device to be redirected and this has
+     * failed.
+     **/
+    signals[CONNECT_FAILED] =
+        g_signal_new("connect-failed",
+                    G_OBJECT_CLASS_TYPE(gobject_class),
+                    G_SIGNAL_RUN_FIRST,
+                    G_STRUCT_OFFSET(SpiceUsbDeviceWidgetClass, connect_failed),
+                    NULL, NULL,
+                    g_cclosure_user_marshal_VOID__BOXED_BOXED,
+                    G_TYPE_NONE,
+                    2,
+                    SPICE_TYPE_USB_DEVICE,
+                    G_TYPE_ERROR);
+}
+
+static void spice_usb_device_widget_init(SpiceUsbDeviceWidget *self)
+{
+    self->priv = SPICE_USB_DEVICE_WIDGET_GET_PRIVATE(self);
+}
+
+/* ------------------------------------------------------------------ */
+/* public api                                                         */
+
+/**
+ * spice_usb_device_widget_new:
+ * @session: #SpiceSession for which to widget will control USB redirection
+ * @device_format_string: (allow-none): String passed to
+ * spice_usb_device_get_description()
+ *
+ * Creates a new widget to control USB redirection.
+ *
+ * Returns: a new #SpiceUsbDeviceWidget instance
+ */
+GtkWidget *spice_usb_device_widget_new(SpiceSession    *session,
+                                       const gchar     *device_format_string)
+{
+    static gboolean init_columns = TRUE;
+    spice_util_get_debug();
+    if (init_columns) {
+        initialize_columns();
+        init_columns = FALSE;
+    }
+    return g_object_new(SPICE_TYPE_USB_DEVICE_WIDGET,
+                        "orientation", GTK_ORIENTATION_VERTICAL,
+                        "session", session,
+                        "device-format-string", device_format_string,
+                        "spacing", 6,
+                        NULL);
+}
+
+/* ------------------------------------------------------------------ */
+/* callbacks                                                          */
+
+static gboolean usb_widget_tree_store_check_redirect_foreach_cb(GtkTreeModel *tree_model,
+                                                                GtkTreePath *path,
+                                                                GtkTreeIter *iter,
+                                                                SpiceUsbDeviceWidget *self,
+                                                                GtkTreeStore *tree_store)
+{
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+    if (!tree_item_is_lun(tree_store, iter)) {
+        SpiceUsbDevice *usb_device;
+        gboolean can_redirect;
+
+        gtk_tree_model_get(tree_model, iter,
+                           COL_ITEM_DATA, (gpointer *)&usb_device,
+                           -1);
+
+        if (spice_usb_device_manager_is_redirecting(priv->manager)) {
+            can_redirect = FALSE;
+        } else {
+            GError *err = NULL;
+
+            can_redirect = spice_usb_device_manager_can_redirect_device(priv->manager,
+                                                                        usb_device, &err);
+
+            /* If we cannot redirect this device, append the error message to
+               err_msg, but only if it is *not* already there! */
+            if (!can_redirect) {
+                if (priv->err_msg) {
+                    if (!strstr(priv->err_msg, err->message)) {
+                        gchar *old_err_msg = priv->err_msg;
+                        priv->err_msg = g_strdup_printf("%s\n%s", priv->err_msg,
+                                                        err->message);
+                        g_free(old_err_msg);
+                    }
+                } else {
+                    priv->err_msg = g_strdup(err->message);
+                }
+            }
+            g_clear_error(&err);
+        }
+        gtk_tree_store_set(tree_store, iter,
+                           COL_CAN_REDIRECT, can_redirect,
+                           -1);
+    }
+    return FALSE; /* continue iterating */
+}
+
+static gboolean usb_widget_tree_store_check_redirect_foreach_cb_usb(GtkTreeModel *tree_model,
+                                                                 GtkTreePath *path, GtkTreeIter *iter,
+                                                                 gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    return usb_widget_tree_store_check_redirect_foreach_cb(
+        tree_model, path, iter, self, priv->usb_tree.tree_store);
+}
+
+#ifdef USE_CD_SHARING
+static gboolean usb_widget_tree_store_check_redirect_foreach_cb_cd(GtkTreeModel *tree_model,
+                                                                 GtkTreePath *path, GtkTreeIter *iter,
+                                                                 gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    return usb_widget_tree_store_check_redirect_foreach_cb(
+        tree_model, path, iter, self, priv->cd_tree.tree_store);
+}
+#endif
+
+static gboolean spice_usb_device_widget_update_status(gpointer user_data)
+{
+    SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+    SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+    gchar *str, *markup_str;
+    const gchar *free_channels_str;
+    int free_channels;
+
+    g_object_get(priv->manager, "free-channels", &free_channels, NULL);
+    free_channels_str = ngettext(_("Select USB devices to redirect (%d free channel)"),
+                                 _("Select USB devices to redirect (%d free channels)"),
+                                 free_channels);
+    str = g_strdup_printf(free_channels_str, free_channels);
+    markup_str = g_strdup_printf("<b>%s</b>", str);
+    gtk_label_set_markup(GTK_LABEL (priv->label), markup_str);
+    g_free(markup_str);
+    g_free(str);
+
+    gtk_tree_model_foreach(GTK_TREE_MODEL(priv->usb_tree.tree_store),
+                           usb_widget_tree_store_check_redirect_foreach_cb_usb, self);
+    gtk_widget_show_all(GTK_WIDGET(priv->usb_tree.tree_view));
+
+#ifdef USE_CD_SHARING
+    gtk_tree_model_foreach(GTK_TREE_MODEL(priv->cd_tree.tree_store),
+                           usb_widget_tree_store_check_redirect_foreach_cb_cd, self);
+    gtk_widget_show_all(GTK_WIDGET(priv->cd_tree.tree_view));
+#endif
+
+    /* Show messages in the info, if necessary */
+    if (priv->err_msg) {
+        spice_usb_device_widget_show_info_bar(self, priv->err_msg,
+                                              GTK_MESSAGE_INFO, priv->icon_warning);
+        g_free(priv->err_msg);
+        priv->err_msg = NULL;
+    } else if ( spice_usb_device_manager_is_redirecting(priv->manager)) {
+        spice_usb_device_widget_show_info_bar(self, _("Redirecting USB Device..."),
+                                              GTK_MESSAGE_INFO, priv->icon_info);
+    } else {
+        spice_usb_device_widget_hide_info_bar(self);
+    }
+
+    if (priv->device_count == 0)
+        spice_usb_device_widget_show_info_bar(self, _("No USB devices detected"),
+                                              GTK_MESSAGE_INFO, priv->icon_info);
+
+    return FALSE;
+}
+
+#endif