summary history branches tags files
commit:646d90bdfcb93966c5b1e2f917b81cf6ea630d99
author:Trevor Bentley
committer:Trevor Bentley
date:Mon Feb 20 17:48:37 2023 +0100
parents:
initial checkin
diff --git a/.gitignore b/.gitignore
line changes: +1/-0
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target

diff --git a/Cargo.lock b/Cargo.lock
line changes: +508/-0
index 0000000..8e50f52
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,508 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.59.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "clap",
+ "env_logger",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "winapi",
+]
+
+[[package]]
+name = "dbus-crossroads"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0"
+dependencies = [
+ "dbus",
+]
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "env_logger"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f8d7ae751e1cb825c840ae5e682f59b098cdfd213c350ac268b61449a5f58a0"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libudev-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "mio"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "qemu-stormcrow"
+version = "0.1.0"
+dependencies = [
+ "dbus",
+ "dbus-crossroads",
+ "mio",
+ "udev",
+ "virt",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "udev"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebdbbd670373442a12fe9ef7aeb53aec4147a5a27a00bbc3ab639f08f48191a"
+dependencies = [
+ "libc",
+ "libudev-sys",
+ "mio",
+ "pkg-config",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "virt"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df0697e60d61398cee42ff0ad350843f05ef84bc34df012653a7e6a3ef067bd"
+dependencies = [
+ "libc",
+ "virt-sys",
+]
+
+[[package]]
+name = "virt-sys"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b88207efda613081369fcb3701283201f9b5745c8f9c09e737921ca0103f5873"
+dependencies = [
+ "bindgen",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"

diff --git a/Cargo.toml b/Cargo.toml
line changes: +13/-0
index 0000000..7351219
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "qemu-stormcrow"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+dbus = "0.9.7"
+dbus-crossroads = "0.5.2"
+mio = {version = "0.8.5", features = ["os-ext"] }
+virt = "0.2.12"
+udev = {version = "0.7.0", features = ["mio08"] }

diff --git a/README.md b/README.md
line changes: +37/-0
index 0000000..1caaeec
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# qemu-stormcrow
+
+## WHAT
+
+A little utility to dynamically passthrough USB devices to running libvirt virtual machines.
+
+## WHY
+
+libvirt supports permanently mapping a USB device to a VM, or dynamically attaching a device once during runtime.  It does not have very good support for temporarily passing through USB devices that are frequently reconnected.  qemu-stormcrow is for that case.
+
+## HOW
+
+qemu-stormcrow should be launched as a daemon first.  `cargo run` in a terminal, or write a systemd service, or spawn it in the background from a script, or whatever.  It does not self-daemonize.
+
+A USB device with a (=Vendor ID=, =Product ID=) pair is registered for a running libvirt VM via D-Bus:
+
+```bash
+$ dbus-send --type=method_call --print-reply --dest=com.stormcrow.device /device com.stormcrow.device.Add string:<VM> string:<VID> string:<PID>
+```
+
+qemu-stormcrow monitors the udev subsystem for attach/remove events of the VID/PID pair.  When one is attached, qemu-stormcrow generates a libvirt hostdev XML snippet for the device and attaches it to the running VM.  Likewise, it detaches the hostdev device when removed.
+
+When finished, the device can be unregistered.  qemu-stormcrow will no longer monitor for such devices:
+
+```bash
+$ dbus-send --type=method_call --print-reply --dest=com.stormcrow.device /device com.stormcrow.device.Remove string:<VM> string:<VID> string:<PID>
+```
+
+qemu-stormcrow can be shutdown via D-Bus as well:
+
+```bash
+$ dbus-send --type=method_call --print-reply --dest=com.stormcrow.device /device com.stormcrow.device.Quit
+```
+
+## SHOULD I USE THIS?
+
+No.  It's a hacky little script for personal use.

diff --git a/src/main.rs b/src/main.rs
line changes: +291/-0
index 0000000..57ede47
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,291 @@
+use dbus::blocking::Connection as DbusConnection;
+use dbus_crossroads::{Context, Crossroads};
+use mio::{Events, Interest, Poll, Token};
+use std::collections::{BTreeMap, HashSet};
+use std::error::Error;
+use std::io::{self, Read};
+use std::path::PathBuf;
+use std::sync::mpsc::{channel, Receiver, Sender};
+use std::thread;
+use std::time::Duration;
+use udev::MonitorBuilder;
+use virt::connect::Connect;
+use virt::domain::Domain;
+
+#[derive(Debug)]
+pub enum DbusCommand {
+    Add,
+    Remove,
+    Shutdown,
+}
+
+#[derive(Eq, PartialEq, Hash, Clone, Debug)]
+pub struct UsbFilter {
+    vid: Option<String>,
+    pid: Option<String>,
+}
+
+struct DbusDevice {
+    sender: Sender<(DbusCommand, String, UsbFilter)>,
+}
+
+// $ dbus-send --type=method_call --print-reply --dest=com.stormcrow.device /device com.stormcrow.device.Add string:<VM> string:<VID> string:<PID>
+fn dbus_server(sender: Sender<(DbusCommand, String, UsbFilter)>) -> Result<(), Box<dyn Error>> {
+    let c = DbusConnection::new_session()?;
+    c.request_name("com.stormcrow.device", false, true, false)?;
+    let mut cr = Crossroads::new();
+    let iface_token = cr.register("com.stormcrow.device", |b| {
+        b.method(
+            "Add",
+            ("vm", "vid", "pid"),
+            ("reply",),
+            move |_ctx: &mut Context,
+                  dev: &mut DbusDevice,
+                  (vm, vid, pid): (String, String, String)| {
+                println!("Incoming Add call for {}:{}!", vid, pid);
+                let filter = UsbFilter {
+                    vid: match vid.len() == 4 {
+                        true => Some(vid),
+                        _ => None,
+                    },
+                    pid: match pid.len() == 4 {
+                        true => Some(pid),
+                        _ => None,
+                    },
+                };
+                dev.sender
+                    .send((DbusCommand::Add, vm, filter))
+                    .expect("failed to transmit from dbus channel");
+                let reply = "OK";
+                Ok((reply,))
+            },
+        );
+        b.method(
+            "Remove",
+            ("vm", "vid", "pid"),
+            ("reply",),
+            move |_ctx: &mut Context,
+                  dev: &mut DbusDevice,
+                  (vm, vid, pid): (String, String, String)| {
+                println!("Incoming Remove call for {}:{}!", vid, pid);
+                let filter = UsbFilter {
+                    vid: match vid.len() == 4 {
+                        true => Some(vid),
+                        _ => None,
+                    },
+                    pid: match pid.len() == 4 {
+                        true => Some(pid),
+                        _ => None,
+                    },
+                };
+                dev.sender
+                    .send((DbusCommand::Remove, vm, filter))
+                    .expect("failed to transmit from dbus channel");
+                let reply = "OK";
+                Ok((reply,))
+            },
+        );
+        b.method(
+            "Quit",
+            (),
+            ("reply",),
+            move |_ctx: &mut Context, dev: &mut DbusDevice, (): ()| {
+                dev.sender
+                    .send((
+                        DbusCommand::Shutdown,
+                        "".into(),
+                        UsbFilter {
+                            vid: None,
+                            pid: None,
+                        },
+                    ))
+                    .expect("failed to transmit from dbus channel");
+                Ok(("BYE",))
+            },
+        );
+    });
+
+    cr.insert("/device", &[iface_token], DbusDevice { sender });
+
+    // Serve clients forever.
+    cr.serve(&c)?;
+    Ok(())
+}
+
+fn usb_xml(vid: &str, pid: &str, bus: &str, dev: &str) -> String {
+    format!(
+        r"
+<hostdev mode='subsystem' type='usb'>
+  <source>
+    <vendor id='0x{}'/>
+    <product id='0x{}'/>
+    <address bus='{}' device='{}'/>
+  </source>
+</hostdev>
+",
+        vid, pid, bus, dev
+    )
+}
+
+pub fn poll(
+    mut socket: udev::MonitorSocket,
+    receiver: Receiver<(DbusCommand, String, UsbFilter)>,
+) -> io::Result<()> {
+    let mut poll = Poll::new()?;
+    let mut events = Events::with_capacity(1024);
+
+    let mut filters = BTreeMap::<String, HashSet<UsbFilter>>::new();
+    let mut sysdevs = BTreeMap::<PathBuf, UsbFilter>::new();
+    let mut xmls = BTreeMap::<String, Vec<(PathBuf, String)>>::new();
+
+    let uri = "qemu:///system";
+    println!("Attempting to connect to hypervisor: '{}'...", uri);
+    let mut conn = match Connect::open(uri) {
+        Ok(c) => c,
+        Err(e) => panic!("No connection to hypervisor: {}", e),
+    };
+
+    poll.registry().register(
+        &mut socket,
+        Token(0),
+        Interest::READABLE | Interest::WRITABLE,
+    )?;
+
+    println!("Polling udev monitor...");
+    'event: loop {
+        poll.poll(&mut events, Some(Duration::from_millis(200)))?;
+        while let Ok(msg) = receiver.try_recv() {
+            match msg.0 {
+                DbusCommand::Shutdown => {
+                    break 'event;
+                }
+                DbusCommand::Add => {
+                    let vm = msg.1;
+                    let filter = msg.2;
+                    if !filters.contains_key(&vm) {
+                        filters.insert(vm.clone(), HashSet::new());
+                    }
+                    if let Some(usb_filters) = filters.get_mut(&vm) {
+                        if !usb_filters.contains(&filter) {
+                            println!("udev add: {:?}:{:?}", filter.vid, filter.pid);
+                            usb_filters.insert(filter);
+                        }
+                    }
+                }
+                DbusCommand::Remove => {
+                    let vm = msg.1;
+                    let filter = msg.2;
+                    if let Some(usb_filters) = filters.get_mut(&vm) {
+                        if usb_filters.contains(&filter) {
+                            println!("udev rem: {:?}:{:?}", filter.vid, filter.pid);
+                            usb_filters.remove(&filter);
+                        }
+                    }
+                }
+            }
+        }
+
+        for event in &events {
+            if event.token() == Token(0) && event.is_writable() {
+                socket.iter().for_each(|x| {
+                    let syspath = x.device().syspath().to_owned();
+                    let mut vidpath = syspath.clone();
+                    let mut pidpath = syspath.clone();
+                    let mut buspath = syspath.clone();
+                    let mut devpath = syspath.clone();
+                    vidpath.push("idVendor");
+                    pidpath.push("idProduct");
+                    buspath.push("busnum");
+                    devpath.push("devnum");
+                    match x.event_type() {
+                        udev::EventType::Add => {
+                            let mut usb_vid = String::new();
+                            let mut usb_pid = String::new();
+                            let mut usb_bus = String::new();
+                            let mut usb_dev = String::new();
+                            let mut f = std::fs::File::open(vidpath).expect("couldn't open USB vendor");
+                            f.read_to_string(&mut usb_vid).expect("failed to read USB vendor");
+                            let mut f = std::fs::File::open(pidpath).expect("couldn't open USB product");
+                            f.read_to_string(&mut usb_pid).expect("failed to read USB vendor");
+                            let mut f = std::fs::File::open(buspath).expect("couldn't open USB bus");
+                            f.read_to_string(&mut usb_bus).expect("failed to read USB vendor");
+                            let mut f = std::fs::File::open(devpath).expect("couldn't open USB device");
+                            f.read_to_string(&mut usb_dev).expect("failed to read USB vendor");
+                            let usb_vid = usb_vid.trim();
+                            let usb_pid = usb_pid.trim();
+                            let usb_bus = usb_bus.trim();
+                            let usb_dev = usb_dev.trim();
+                            let usb_filter = UsbFilter {vid: Some(usb_vid.into()), pid: Some(usb_pid.into())};
+                            for (vm, vm_filter) in filters.iter() {
+                                if vm_filter.contains(&usb_filter) {
+                                    println!("Adding syspath: {} for vm {} [VID:{} PID:{}]", syspath.display(), vm, usb_vid, usb_pid);
+                                    sysdevs.insert(syspath.clone(), usb_filter.clone());
+                                    if let Ok(domain) = Domain::lookup_by_name(&conn, vm) {
+                                        let xml = usb_xml(usb_vid, usb_pid, usb_bus, usb_dev);
+                                        domain.attach_device(&xml).expect("failed to attach USB XML!");
+                                        if !xmls.contains_key(vm) {
+                                            xmls.insert(vm.to_owned(), Vec::new());
+                                        }
+                                        if let Some(vm_xmls) = xmls.get_mut(vm) {
+                                            vm_xmls.push((syspath.clone(), xml));
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        udev::EventType::Remove => {
+                            match sysdevs.contains_key(&syspath) {
+                                true => {
+                                    println!("Removing syspath: {}", syspath.display());
+                                    sysdevs.remove(&syspath);
+                                    for (vm, vm_xmls) in xmls.iter_mut() {
+                                        for (vm_syspath, xml_str) in vm_xmls.iter() {
+                                            if vm_syspath == &syspath {
+                                                if let Ok(domain) = Domain::lookup_by_name(&conn, vm) {
+                                                    if let Err(e) = domain.detach_device(xml_str) {
+                                                        println!("WARNING: failed to hot-unplug from domain {}: {}", vm, e);
+                                                    }
+                                                }
+                                            }
+                                        }
+                                        vm_xmls.retain(|i| i.0 != syspath);
+                                    }
+                                },
+                                false => {
+                                },
+                            }
+                        },
+                        _ => {},
+                    }
+                });
+            }
+        }
+    }
+
+    println!("Shutting down by request.");
+    if let Err(e) = conn.close() {
+        panic!("Failed to disconnect from hypervisor: {}", e);
+    }
+    Ok(())
+}
+
+fn main() {
+    println!("Starting qemu-stormcrow.");
+
+    println!("Starting dbus monitor...");
+    let (sender, receiver) = channel::<(DbusCommand, String, UsbFilter)>();
+    thread::spawn(move || {
+        dbus_server(sender).expect("failed to launch dbus server");
+    });
+
+    println!("Making udev monitor...");
+    let socket = MonitorBuilder::new()
+        .expect("failed to create new udev monitor")
+        .match_subsystem_devtype("usb", "usb_device")
+        .expect("failed to create usb matcher")
+        .listen()
+        .expect("failed to register udev monitor");
+
+    poll(socket, receiver).expect("failed to poll udev monitor");
+    println!("Done!");
+}