Bug 1419685 - [u2f-hid-rs] Implement per-device threads on Windows, remove KeyHandleMatcher r=jcj
authorTim Taubert <ttaubert@mozilla.com>
Wed, 22 Nov 2017 10:13:49 +0100
changeset 437536 30ef5052449ca44f85b5db4daf1e0b7cbb0bb8ae
parent 437535 d28a3ef4867908a8024ba705c1773c82c92171b8
child 437537 9edac493b4d91fa8272c375b3408a886b7f26c32
push id117
push userfmarier@mozilla.com
push dateTue, 28 Nov 2017 20:17:16 +0000
reviewersjcj
bugs1419685
milestone59.0a1
Bug 1419685 - [u2f-hid-rs] Implement per-device threads on Windows, remove KeyHandleMatcher r=jcj This patch rewrites the Windows backend to have one thread per device. As a bonus we get to remove the KeyHandleMatcher. Review: https://github.com/jcjones/u2f-hid-rs/pull/57 https://github.com/jcjones/u2f-hid-rs/commit/296f6707b3da1e098dff866b8bc5d6d734a15515
dom/webauthn/u2f-hid-rs/src/khmatcher.rs
dom/webauthn/u2f-hid-rs/src/lib.rs
dom/webauthn/u2f-hid-rs/src/linux/devicemap.rs
dom/webauthn/u2f-hid-rs/src/macos/devicemap.rs
dom/webauthn/u2f-hid-rs/src/windows/devicemap.rs
dom/webauthn/u2f-hid-rs/src/windows/mod.rs
dom/webauthn/u2f-hid-rs/src/windows/monitor.rs
dom/webauthn/u2f-hid-rs/src/windows/transaction.rs
deleted file mode 100644
--- a/dom/webauthn/u2f-hid-rs/src/khmatcher.rs
+++ /dev/null
@@ -1,156 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-use std::collections::{HashMap, HashSet};
-use std::collections::hash_map::IterMut;
-use std::hash::Hash;
-
-// The KeyHandleMatcher is a helper class that tracks which key handles belong
-// to which device (token). It is initialized with a list of key handles we
-// were passed by the U2F or WebAuthn API.
-//
-// 'a = the lifetime of key handles, we don't want to own them
-//  K = the type we use to identify devices on this platform (path, device_ref)
-pub struct KeyHandleMatcher<'a, K> {
-    key_handles: &'a Vec<Vec<u8>>,
-    map: HashMap<K, Vec<&'a Vec<u8>>>,
-}
-
-impl<'a, K> KeyHandleMatcher<'a, K>
-where
-    K: Clone + Eq + Hash,
-{
-    pub fn new(key_handles: &'a Vec<Vec<u8>>) -> Self {
-        Self {
-            key_handles,
-            map: HashMap::new(),
-        }
-    }
-
-    // `update()` will iterate all devices and ignore the ones we've already
-    // checked. It will then call the given closure exactly once for each key
-    // handle and device combination to let the caller decide whether we have
-    // a match.
-    //
-    // If a device was removed since the last `update()` call we'll remove it
-    // from the internal map as well to ensure we query a device that reuses a
-    // dev_ref or path next time.
-    //
-    // TODO In theory, it might be possible to replace a token between
-    // `update()` calls while reusing the device_ref/path. We should refactor
-    // this part of the code and probably merge the KeyHandleMatcher into the
-    // DeviceMap and Monitor somehow so that we query key handle/token
-    // assignments right when we start tracking a new device.
-    pub fn update<F, V>(&mut self, devices: IterMut<K, V>, is_match: F)
-    where
-        F: Fn(&mut V, &Vec<u8>) -> bool,
-    {
-        // Collect all currently known device references.
-        let mut to_remove: HashSet<K> = self.map.keys().cloned().collect();
-
-        for (dev_ref, device) in devices {
-            // This device is still connected.
-            to_remove.remove(dev_ref);
-
-            // Skip devices we've already seen.
-            if self.map.contains_key(dev_ref) {
-                continue;
-            }
-
-            let mut matches = vec![];
-
-            // Collect all matching key handles.
-            for key_handle in self.key_handles {
-                if is_match(device, key_handle) {
-                    matches.push(key_handle);
-                }
-            }
-
-            self.map.insert((*dev_ref).clone(), matches);
-        }
-
-        // Remove devices that disappeared since the last call.
-        for dev_ref in to_remove {
-            self.map.remove(&dev_ref);
-        }
-    }
-
-    // `get()` allows retrieving key handle/token assignments that were
-    // process by calls to `update()` before.
-    pub fn get(&self, dev_ref: &K) -> &Vec<&'a Vec<u8>> {
-        self.map.get(dev_ref).expect("unknown device")
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::KeyHandleMatcher;
-
-    use std::collections::HashMap;
-
-    #[test]
-    fn test() {
-        // Three key handles.
-        let khs = vec![
-            vec![0x01, 0x02, 0x03, 0x04],
-            vec![0x02, 0x03, 0x04, 0x05],
-            vec![0x03, 0x04, 0x05, 0x06],
-        ];
-
-        // Start with three devices.
-        let mut map = HashMap::new();
-        map.insert("device1", 1);
-        map.insert("device2", 2);
-        map.insert("device3", 3);
-
-        // Assign key handles to devices.
-        let mut khm = KeyHandleMatcher::new(&khs);
-        khm.update(map.iter_mut(), |device, key_handle| *device > key_handle[0]);
-
-        // Check assignments.
-        assert!(khm.get(&"device1").is_empty());
-        assert_eq!(*khm.get(&"device2"), vec![&vec![0x01, 0x02, 0x03, 0x04]]);
-        assert_eq!(
-            *khm.get(&"device3"),
-            vec![&vec![0x01, 0x02, 0x03, 0x04], &vec![0x02, 0x03, 0x04, 0x05]]
-        );
-
-        // Check we don't check a device twice.
-        map.insert("device4", 4);
-        khm.update(map.iter_mut(), |device, key_handle| {
-            assert_eq!(*device, 4);
-            key_handle[0] & 1 == 1
-        });
-
-        // Check assignments.
-        assert_eq!(
-            *khm.get(&"device4"),
-            vec![&vec![0x01, 0x02, 0x03, 0x04], &vec![0x03, 0x04, 0x05, 0x06]]
-        );
-
-        // Remove device #2.
-        map.remove("device2");
-        khm.update(map.iter_mut(), |_, _| {
-            assert!(false);
-            false
-        });
-
-        // Re-insert device #2 matching different key handles.
-        map.insert("device2", 2);
-        khm.update(map.iter_mut(), |device, _| {
-            assert_eq!(*device, 2);
-            true
-        });
-
-        // Check assignments.
-        assert_eq!(
-            *khm.get(&"device2"),
-            vec![
-                &vec![0x01, 0x02, 0x03, 0x04],
-                &vec![0x02, 0x03, 0x04, 0x05],
-                &vec![0x03, 0x04, 0x05, 0x06],
-            ]
-        );
-    }
-}
--- a/dom/webauthn/u2f-hid-rs/src/lib.rs
+++ b/dom/webauthn/u2f-hid-rs/src/lib.rs
@@ -22,19 +22,16 @@ pub mod platform;
 #[cfg(any(target_os = "windows"))]
 #[path = "windows/mod.rs"]
 pub mod platform;
 
 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
 #[path = "stub/mod.rs"]
 pub mod platform;
 
-#[cfg(target_os = "windows")]
-mod khmatcher;
-
 #[macro_use]
 extern crate log;
 extern crate rand;
 extern crate libc;
 extern crate boxfnonce;
 extern crate runloop;
 
 mod consts;
deleted file mode 100644
--- a/dom/webauthn/u2f-hid-rs/src/linux/devicemap.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-use std::collections::hash_map::IterMut;
-use std::collections::HashMap;
-use std::ffi::OsString;
-
-use platform::device::Device;
-use platform::monitor::Event;
-use u2fprotocol::u2f_init_device;
-
-pub struct DeviceMap {
-    map: HashMap<OsString, Device>,
-}
-
-impl DeviceMap {
-    pub fn new() -> Self {
-        Self { map: HashMap::new() }
-    }
-
-    pub fn iter_mut(&mut self) -> IterMut<OsString, Device> {
-        self.map.iter_mut()
-    }
-
-    pub fn process_event(&mut self, event: Event) {
-        match event {
-            Event::Add(path) => self.add(path),
-            Event::Remove(path) => self.remove(path),
-        }
-    }
-
-    fn add(&mut self, path: OsString) {
-        if self.map.contains_key(&path) {
-            return;
-        }
-
-        // Create and try to open the device.
-        if let Ok(mut dev) = Device::new(path.clone()) {
-            if dev.is_u2f() && u2f_init_device(&mut dev) {
-                self.map.insert(path, dev);
-            }
-        }
-    }
-
-    fn remove(&mut self, path: OsString) {
-        // Ignore errors.
-        let _ = self.map.remove(&path);
-    }
-}
deleted file mode 100644
--- a/dom/webauthn/u2f-hid-rs/src/macos/devicemap.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-use std::collections::hash_map::IterMut;
-use std::collections::HashMap;
-
-use u2fprotocol::u2f_init_device;
-
-use platform::monitor::Event;
-use platform::device::Device;
-use platform::iokit::*;
-
-pub struct DeviceMap {
-    map: HashMap<IOHIDDeviceRef, Device>,
-}
-
-impl DeviceMap {
-    pub fn new() -> Self {
-        Self { map: HashMap::new() }
-    }
-
-    pub fn iter_mut(&mut self) -> IterMut<IOHIDDeviceRef, Device> {
-        self.map.iter_mut()
-    }
-
-    pub fn process_event(&mut self, event: Event) {
-        match event {
-            Event::Add(dev) => self.add(dev),
-            Event::Remove(dev) => self.remove(dev),
-        }
-    }
-
-    fn add(&mut self, device_ref: IOHIDDeviceRef) {
-        if self.map.contains_key(&device_ref) {
-            return;
-        }
-
-        // Create the device.
-        let mut dev = Device::new(device_ref);
-
-        if u2f_init_device(&mut dev) {
-            self.map.insert(device_ref, dev);
-        }
-    }
-
-    fn remove(&mut self, device_ref: IOHIDDeviceRef) {
-        // Ignore errors.
-        let _ = self.map.remove(&device_ref);
-    }
-}
deleted file mode 100644
--- a/dom/webauthn/u2f-hid-rs/src/windows/devicemap.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-use std::collections::hash_map::IterMut;
-use std::collections::HashMap;
-
-use platform::device::Device;
-use platform::monitor::Event;
-use u2fprotocol::u2f_init_device;
-
-pub struct DeviceMap {
-    map: HashMap<String, Device>,
-}
-
-impl DeviceMap {
-    pub fn new() -> Self {
-        Self { map: HashMap::new() }
-    }
-
-    pub fn iter_mut(&mut self) -> IterMut<String, Device> {
-        self.map.iter_mut()
-    }
-
-    pub fn process_event(&mut self, event: Event) {
-        match event {
-            Event::Add(path) => self.add(path),
-            Event::Remove(path) => self.remove(path),
-        }
-    }
-
-    fn add(&mut self, path: String) {
-        if self.map.contains_key(&path) {
-            return;
-        }
-
-        // Create and try to open the device.
-        if let Ok(mut dev) = Device::new(path.clone()) {
-            if dev.is_u2f() && u2f_init_device(&mut dev) {
-                self.map.insert(path, dev);
-            }
-        }
-    }
-
-    fn remove(&mut self, path: String) {
-        // Ignore errors.
-        let _ = self.map.remove(&path);
-    }
-}
--- a/dom/webauthn/u2f-hid-rs/src/windows/mod.rs
+++ b/dom/webauthn/u2f-hid-rs/src/windows/mod.rs
@@ -1,170 +1,149 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use std::thread;
 use std::time::Duration;
 
 mod device;
-mod devicemap;
 mod monitor;
+mod transaction;
 mod winapi;
 
 use consts::PARAMETER_SIZE;
-use khmatcher::KeyHandleMatcher;
-use runloop::RunLoop;
+use platform::device::Device;
+use platform::transaction::Transaction;
 use util::{io_err, OnceCallback};
-use u2fprotocol::{u2f_register, u2f_sign, u2f_is_keyhandle_valid};
+use u2fprotocol::{u2f_init_device, u2f_register, u2f_sign, u2f_is_keyhandle_valid};
 
-use self::devicemap::DeviceMap;
-use self::monitor::Monitor;
-
+#[derive(Default)]
 pub struct PlatformManager {
-    // Handle to the thread loop.
-    thread: Option<RunLoop>,
+    transaction: Option<Transaction>,
 }
 
 impl PlatformManager {
     pub fn new() -> Self {
-        Self { thread: None }
+        Default::default()
     }
 
     pub fn register(
         &mut self,
         timeout: u64,
         challenge: Vec<u8>,
         application: Vec<u8>,
         key_handles: Vec<Vec<u8>>,
         callback: OnceCallback<Vec<u8>>,
     ) {
         // Abort any prior register/sign calls.
         self.cancel();
 
         let cbc = callback.clone();
 
-        let thread = RunLoop::new_with_timeout(
-            move |alive| {
-                let mut devices = DeviceMap::new();
-                let monitor = try_or!(Monitor::new(), |e| callback.call(Err(e)));
-                let mut matches = KeyHandleMatcher::new(&key_handles);
+        let transaction = Transaction::new(timeout, cbc.clone(), move |path, alive| {
+            // Create a new device.
+            let dev = &mut match Device::new(path) {
+                Ok(dev) => dev,
+                _ => return,
+            };
 
-                while alive() && monitor.alive() {
-                    // Add/remove devices.
-                    for event in monitor.events() {
-                        devices.process_event(event);
-                    }
+            // Try initializing it.
+            if !dev.is_u2f() || !u2f_init_device(dev) {
+                return;
+            }
 
-                    // Query newly added devices.
-                    matches.update(devices.iter_mut(), |device, key_handle| {
-                        u2f_is_keyhandle_valid(device, &challenge, &application, key_handle)
-                            .unwrap_or(false /* no match on failure */)
-                    });
+            // Iterate the exclude list and see if there are any matches.
+            // Abort the state machine if we found a valid key handle.
+            if key_handles.iter().any(|key_handle| {
+                u2f_is_keyhandle_valid(dev, &challenge, &application, key_handle)
+                    .unwrap_or(false) /* no match on failure */
+            })
+            {
+                return;
+            }
 
-                    // Iterate all devices that don't match any of the handles
-                    // in the exclusion list and try to register.
-                    for (path, device) in devices.iter_mut() {
-                        if matches.get(path).is_empty() {
-                            if let Ok(bytes) = u2f_register(device, &challenge, &application) {
-                                callback.call(Ok(bytes));
-                                return;
-                            }
-                        }
-                    }
-
-                    // Wait a little before trying again.
-                    thread::sleep(Duration::from_millis(100));
+            while alive() {
+                if let Ok(bytes) = u2f_register(dev, &challenge, &application) {
+                    callback.call(Ok(bytes));
+                    break;
                 }
 
-                callback.call(Err(io_err("aborted or timed out")));
-            },
-            timeout,
-        );
+                // Sleep a bit before trying again.
+                thread::sleep(Duration::from_millis(100));
+            }
+        });
 
-        self.thread = Some(try_or!(thread, |_| {
-            cbc.call(Err(io_err("couldn't create runloop")));
+        self.transaction = Some(try_or!(transaction, |_| {
+            cbc.call(Err(io_err("couldn't create transaction")));
         }));
     }
 
     pub fn sign(
         &mut self,
         timeout: u64,
         challenge: Vec<u8>,
         application: Vec<u8>,
         key_handles: Vec<Vec<u8>>,
         callback: OnceCallback<(Vec<u8>, Vec<u8>)>,
     ) {
         // Abort any prior register/sign calls.
         self.cancel();
 
         let cbc = callback.clone();
 
-        let thread = RunLoop::new_with_timeout(
-            move |alive| {
-                let mut devices = DeviceMap::new();
-                let monitor = try_or!(Monitor::new(), |e| callback.call(Err(e)));
-                let mut matches = KeyHandleMatcher::new(&key_handles);
+        let transaction = Transaction::new(timeout, cbc.clone(), move |path, alive| {
+            // Create a new device.
+            let dev = &mut match Device::new(path) {
+                Ok(dev) => dev,
+                _ => return,
+            };
 
-                while alive() && monitor.alive() {
-                    // Add/remove devices.
-                    for event in monitor.events() {
-                        devices.process_event(event);
-                    }
-
-                    // Query newly added devices.
-                    matches.update(devices.iter_mut(), |device, key_handle| {
-                        u2f_is_keyhandle_valid(device, &challenge, &application, key_handle)
-                            .unwrap_or(false /* no match on failure */)
-                    });
-
-                    // Iterate all devices.
-                    for (path, device) in devices.iter_mut() {
-                        let key_handles = matches.get(path);
+            // Try initializing it.
+            if !dev.is_u2f() || !u2f_init_device(dev) {
+                return;
+            }
 
-                        // If the device matches none of the given key handles
-                        // then just make it blink with bogus data.
-                        if key_handles.is_empty() {
-                            let blank = vec![0u8; PARAMETER_SIZE];
-                            if let Ok(_) = u2f_register(device, &blank, &blank) {
-                                callback.call(Err(io_err("invalid key")));
-                                return;
-                            }
-
-                            continue;
-                        }
+            // Find all matching key handles.
+            let key_handles = key_handles
+                .iter()
+                .filter(|key_handle| {
+                    u2f_is_keyhandle_valid(dev, &challenge, &application, key_handle)
+                        .unwrap_or(false) /* no match on failure */
+                })
+                .collect::<Vec<_>>();
 
-                        // Otherwise, try to sign.
-                        for key_handle in key_handles {
-                            if let Ok(bytes) = u2f_sign(
-                                device,
-                                &challenge,
-                                &application,
-                                key_handle,
-                            )
-                            {
-                                callback.call(Ok((key_handle.to_vec(), bytes)));
-                                return;
-                            }
+            while alive() {
+                // If the device matches none of the given key handles
+                // then just make it blink with bogus data.
+                if key_handles.is_empty() {
+                    let blank = vec![0u8; PARAMETER_SIZE];
+                    if let Ok(_) = u2f_register(dev, &blank, &blank) {
+                        callback.call(Err(io_err("invalid key")));
+                        break;
+                    }
+                } else {
+                    // Otherwise, try to sign.
+                    for key_handle in &key_handles {
+                        if let Ok(bytes) = u2f_sign(dev, &challenge, &application, key_handle) {
+                            callback.call(Ok((key_handle.to_vec(), bytes)));
+                            break;
                         }
                     }
-
-                    // Wait a little before trying again.
-                    thread::sleep(Duration::from_millis(100));
                 }
 
-                callback.call(Err(io_err("aborted or timed out")));
-            },
-            timeout,
-        );
+                // Sleep a bit before trying again.
+                thread::sleep(Duration::from_millis(100));
+            }
+        });
 
-        self.thread = Some(try_or!(thread, |_| {
-            cbc.call(Err(io_err("couldn't create runloop")));
+        self.transaction = Some(try_or!(transaction, |_| {
+            cbc.call(Err(io_err("couldn't create transaction")));
         }));
     }
 
     // This might block.
     pub fn cancel(&mut self) {
-        if let Some(thread) = self.thread.take() {
-            thread.cancel();
+        if let Some(mut transaction) = self.transaction.take() {
+            transaction.cancel();
         }
     }
 }
--- a/dom/webauthn/u2f-hid-rs/src/windows/monitor.rs
+++ b/dom/webauthn/u2f-hid-rs/src/windows/monitor.rs
@@ -1,86 +1,89 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-use std::collections::HashSet;
-use std::error::Error;
+use platform::winapi::DeviceInfoSet;
+use runloop::RunLoop;
+use std::collections::{HashMap, HashSet};
 use std::io;
 use std::iter::FromIterator;
-use std::sync::mpsc::{channel, Receiver, TryIter};
+use std::sync::Arc;
 use std::thread;
 use std::time::Duration;
 
-use runloop::RunLoop;
-use super::winapi::DeviceInfoSet;
-
-pub fn io_err(msg: &str) -> io::Error {
-    io::Error::new(io::ErrorKind::Other, msg)
-}
-
-pub fn to_io_err<T: Error>(err: T) -> io::Error {
-    io_err(err.description())
-}
-
-pub enum Event {
-    Add(String),
-    Remove(String),
-}
-
-pub struct Monitor {
-    // Receive events from the thread.
-    rx: Receiver<Event>,
-    // Handle to the thread loop.
-    thread: RunLoop,
+pub struct Monitor<F>
+where
+    F: Fn(String, &Fn() -> bool) + Sync,
+{
+    runloops: HashMap<String, RunLoop>,
+    new_device_cb: Arc<F>,
 }
 
-impl Monitor {
-    pub fn new() -> io::Result<Self> {
-        let (tx, rx) = channel();
-
-        let thread = RunLoop::new(move |alive| -> io::Result<()> {
-            let mut stored = HashSet::new();
-
-            while alive() {
-                let device_info_set = DeviceInfoSet::new()?;
-                let devices = HashSet::from_iter(device_info_set.devices());
+impl<F> Monitor<F>
+where
+    F: Fn(String, &Fn() -> bool) + Send + Sync + 'static,
+{
+    pub fn new(new_device_cb: F) -> Self {
+        Self {
+            runloops: HashMap::new(),
+            new_device_cb: Arc::new(new_device_cb),
+        }
+    }
 
-                // Remove devices that are gone.
-                for path in stored.difference(&devices) {
-                    tx.send(Event::Remove(path.clone())).map_err(to_io_err)?;
-                }
+    pub fn run(&mut self, alive: &Fn() -> bool) -> io::Result<()> {
+        let mut stored = HashSet::new();
+
+        while alive() {
+            let device_info_set = DeviceInfoSet::new()?;
+            let devices = HashSet::from_iter(device_info_set.devices());
 
-                // Add devices that were plugged in.
-                for path in devices.difference(&stored) {
-                    tx.send(Event::Add(path.clone())).map_err(to_io_err)?;
-                }
+            // Remove devices that are gone.
+            for path in stored.difference(&devices) {
+                self.remove_device(path);
+            }
 
-                // Remember the new set.
-                stored = devices;
-
-                // Wait a little before looking for devices again.
-                thread::sleep(Duration::from_millis(100));
+            // Add devices that were plugged in.
+            for path in devices.difference(&stored) {
+                self.add_device(path);
             }
 
-            Ok(())
-        })?;
+            // Remember the new set.
+            stored = devices;
 
-        Ok(Self {
-            rx: rx,
-            thread: thread,
-        })
+            // Wait a little before looking for devices again.
+            thread::sleep(Duration::from_millis(100));
+        }
+
+        // Remove all tracked devices.
+        self.remove_all_devices();
+
+        Ok(())
     }
 
-    pub fn events<'a>(&'a self) -> TryIter<'a, Event> {
-        self.rx.try_iter()
+    fn add_device(&mut self, path: &String) {
+        let f = self.new_device_cb.clone();
+        let path = path.clone();
+        let key = path.clone();
+
+        let runloop = RunLoop::new(move |alive| if alive() {
+            f(path, alive);
+        });
+
+        if let Ok(runloop) = runloop {
+            self.runloops.insert(key, runloop);
+        }
     }
 
-    pub fn alive(&self) -> bool {
-        self.thread.alive()
+    fn remove_device(&mut self, path: &String) {
+        if let Some(runloop) = self.runloops.remove(path) {
+            runloop.cancel();
+        }
+    }
+
+    fn remove_all_devices(&mut self) {
+        while !self.runloops.is_empty() {
+            let path = self.runloops.keys().next().unwrap().clone();
+            self.remove_device(&path);
+        }
     }
 }
-
-impl Drop for Monitor {
-    fn drop(&mut self) {
-        self.thread.cancel();
-    }
-}
new file mode 100644
--- /dev/null
+++ b/dom/webauthn/u2f-hid-rs/src/windows/transaction.rs
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::io;
+use util::{io_err, OnceCallback};
+
+pub struct Transaction {
+    // Handle to the thread loop.
+    thread: Option<RunLoop>,
+}
+
+impl Transaction {
+    pub fn new<F, T>(timeout: u64, callback: OnceCallback<T>, new_device_cb: F) -> io::Result<Self>
+    where
+        F: Fn(String, &Fn() -> bool) + Sync + Send + 'static,
+        T: 'static,
+    {
+        let thread = RunLoop::new_with_timeout(
+            move |alive| {
+                // Create a new device monitor.
+                let mut monitor = Monitor::new(new_device_cb);
+
+                // Start polling for new devices.
+                try_or!(monitor.run(alive), |e| callback.call(Err(e)));
+
+                // Send an error, if the callback wasn't called already.
+                callback.call(Err(io_err("aborted or timed out")));
+            },
+            timeout,
+        )?;
+
+        Ok(Self { thread: Some(thread) })
+    }
+
+    pub fn cancel(&mut self) {
+        // This must never be None.
+        self.thread.take().unwrap().cancel();
+    }
+}