Bug 1398268 - [u2f-hid-rs] Rewrite macOS IOHIDManager communication and state machine r=jcj

Review: https://github.com/jcjones/u2f-hid-rs/pull/52
This commit is contained in:
Tim Taubert 2017-11-14 11:39:29 +01:00
Родитель ec80cf873c
Коммит b350f42d65
8 изменённых файлов: 413 добавлений и 343 удалений

1
dom/webauthn/u2f-hid-rs/.gitignore поставляемый
Просмотреть файл

@ -1,6 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
**/*.rs.bk
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock

Просмотреть файл

@ -27,6 +27,9 @@ pub mod platform;
#[path = "stub/mod.rs"]
pub mod platform;
#[cfg(not(any(target_os = "macos")))]
mod khmatcher;
#[macro_use]
extern crate log;
extern crate rand;
@ -35,7 +38,6 @@ extern crate boxfnonce;
extern crate runloop;
mod consts;
mod khmatcher;
mod u2ftypes;
mod u2fprotocol;

Просмотреть файл

@ -2,70 +2,31 @@
* 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/. */
extern crate libc;
extern crate log;
use std::fmt;
use std::io;
use std::io::{Read, Write};
use std::slice;
use std::sync::mpsc::{channel, Sender, Receiver, RecvTimeoutError};
use std::time::Duration;
use core_foundation_sys::base::*;
use libc::c_void;
use consts::{CID_BROADCAST, HID_RPT_SIZE};
use core_foundation_sys::base::*;
use platform::iokit::*;
use std::{fmt, io};
use std::io::{Read, Write};
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::time::Duration;
use u2ftypes::U2FDevice;
use super::iokit::*;
const READ_TIMEOUT: u64 = 15;
pub struct Device {
device_ref: IOHIDDeviceRef,
cid: [u8; 4],
report_rx: Receiver<Vec<u8>>,
report_send_void: *mut c_void,
scratch_buf_ptr: *mut u8,
}
impl Device {
pub fn new(device_ref: IOHIDDeviceRef) -> Self {
let (report_tx, report_rx) = channel();
let report_send_void = Box::into_raw(Box::new(report_tx)) as *mut c_void;
let scratch_buf = [0; HID_RPT_SIZE];
let scratch_buf_ptr = Box::into_raw(Box::new(scratch_buf)) as *mut u8;
unsafe {
IOHIDDeviceRegisterInputReportCallback(
device_ref,
scratch_buf_ptr,
HID_RPT_SIZE as CFIndex,
read_new_data_cb,
report_send_void,
);
}
pub fn new(device_ref: IOHIDDeviceRef, report_rx: Receiver<Vec<u8>>) -> Self {
Self {
device_ref,
cid: CID_BROADCAST,
report_rx,
report_send_void,
scratch_buf_ptr,
}
}
}
impl Drop for Device {
fn drop(&mut self) {
debug!("Dropping U2F device {}", self);
unsafe {
// Re-allocate raw pointers for destruction.
let _ = Box::from_raw(self.report_send_void as *mut Sender<Vec<u8>>);
let _ = Box::from_raw(self.scratch_buf_ptr as *mut [u8; HID_RPT_SIZE]);
}
}
}
@ -148,45 +109,3 @@ impl U2FDevice for Device {
self.cid = cid;
}
}
// This is called from the RunLoop thread
extern "C" fn read_new_data_cb(
context: *mut c_void,
_: IOReturn,
_: *mut c_void,
report_type: IOHIDReportType,
report_id: u32,
report: *mut u8,
report_len: CFIndex,
) {
unsafe {
let tx = &mut *(context as *mut Sender<Vec<u8>>);
trace!(
"read_new_data_cb type={} id={} report={:?} len={}",
report_type,
report_id,
report,
report_len
);
let report_len = report_len as usize;
if report_len > HID_RPT_SIZE {
warn!(
"read_new_data_cb got too much data! {} > {}",
report_len,
HID_RPT_SIZE
);
return;
}
let data = slice::from_raw_parts(report, report_len).to_vec();
if let Err(e) = tx.send(data) {
// TOOD: This happens when the channel closes before this thread
// does. This is pretty common, but let's deal with stopping
// properly later.
warn!("Problem returning read_new_data_cb data for thread: {}", e);
};
}
}

Просмотреть файл

@ -5,17 +5,14 @@
extern crate log;
extern crate libc;
use std::io;
use super::iokit::*;
use consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID};
use core_foundation_sys::base::*;
use core_foundation_sys::dictionary::*;
use core_foundation_sys::number::*;
use core_foundation_sys::runloop::*;
use core_foundation_sys::string::*;
use consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID};
use util::io_err;
use libc::c_void;
use platform::iokit::{CFRunLoopObserverContext, CFRunLoopObserverCreate};
pub struct IOHIDDeviceMatcher {
dict: CFDictionaryRef,
@ -92,36 +89,48 @@ impl Drop for IOHIDDeviceMatcher {
}
}
pub struct IOHIDManager {
manager: IOHIDManagerRef,
pub struct CFRunLoopEntryObserver {
observer: CFRunLoopObserverRef,
// Keep alive until the observer goes away.
context_ptr: *mut CFRunLoopObserverContext,
}
impl IOHIDManager {
pub fn new() -> io::Result<Self> {
let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) };
impl CFRunLoopEntryObserver {
pub fn new(callback: CFRunLoopObserverCallBack, context: *mut c_void) -> Self {
let context = CFRunLoopObserverContext::new(context);
let context_ptr = Box::into_raw(Box::new(context));
let rv = unsafe { IOHIDManagerOpen(manager, kIOHIDManagerOptionNone) };
if rv != 0 {
return Err(io_err("Couldn't open HID Manager"));
}
unsafe {
IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)
let observer = unsafe {
CFRunLoopObserverCreate(
kCFAllocatorDefault,
kCFRunLoopEntry,
false as Boolean,
0,
callback,
context_ptr,
)
};
Ok(Self { manager })
Self {
observer,
context_ptr,
}
}
pub fn get(&self) -> IOHIDManagerRef {
self.manager
pub fn add_to_current_runloop(&self) {
unsafe {
CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.observer, kCFRunLoopDefaultMode)
};
}
}
impl Drop for IOHIDManager {
impl Drop for CFRunLoopEntryObserver {
fn drop(&mut self) {
let rv = unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) };
if rv != 0 {
warn!("Couldn't close the HID Manager");
}
unsafe {
CFRelease(self.observer as *mut c_void);
// Drop the CFRunLoopObserverContext.
let _ = Box::from_raw(self.context_ptr);
};
}
}

Просмотреть файл

@ -7,11 +7,12 @@
extern crate core_foundation_sys;
extern crate libc;
use libc::c_void;
use core_foundation_sys::base::{CFIndex, CFAllocatorRef};
use core_foundation_sys::base::{Boolean, CFIndex, CFAllocatorRef, CFOptionFlags};
use core_foundation_sys::string::CFStringRef;
use core_foundation_sys::runloop::CFRunLoopRef;
use core_foundation_sys::runloop::{CFRunLoopRef, CFRunLoopObserverRef, CFRunLoopObserverCallBack};
use core_foundation_sys::dictionary::CFDictionaryRef;
use libc::c_void;
use std::ops::Deref;
type IOOptionBits = u32;
@ -28,7 +29,7 @@ pub type IOHIDDeviceCallback = extern "C" fn(context: *mut c_void,
pub type IOHIDReportType = IOOptionBits;
pub type IOHIDReportCallback = extern "C" fn(context: *mut c_void,
result: IOReturn,
sender: *mut c_void,
sender: IOHIDDeviceRef,
report_type: IOHIDReportType,
report_id: u32,
report: *mut u8,
@ -50,8 +51,51 @@ pub struct IOHIDDeviceRef(*const c_void);
unsafe impl Send for IOHIDDeviceRef {}
unsafe impl Sync for IOHIDDeviceRef {}
pub struct SendableRunLoop(pub CFRunLoopRef);
unsafe impl Send for SendableRunLoop {}
impl Deref for SendableRunLoop {
type Target = CFRunLoopRef;
fn deref(&self) -> &CFRunLoopRef {
&self.0
}
}
#[repr(C)]
pub struct CFRunLoopObserverContext {
pub version: CFIndex,
pub info: *mut c_void,
pub retain: Option<extern "C" fn(info: *const c_void) -> *const c_void>,
pub release: Option<extern "C" fn(info: *const c_void)>,
pub copyDescription: Option<extern "C" fn(info: *const c_void) -> CFStringRef>,
}
impl CFRunLoopObserverContext {
pub fn new(context: *mut c_void) -> Self {
Self {
version: 0 as CFIndex,
info: context,
retain: None,
release: None,
copyDescription: None,
}
}
}
#[link(name = "IOKit", kind = "framework")]
extern "C" {
// CFRunLoop
pub fn CFRunLoopObserverCreate(
allocator: CFAllocatorRef,
activities: CFOptionFlags,
repeats: Boolean,
order: CFIndex,
callout: CFRunLoopObserverCallBack,
context: *mut CFRunLoopObserverContext,
) -> CFRunLoopObserverRef;
// IOHIDManager
pub fn IOHIDManagerCreate(
allocator: CFAllocatorRef,
@ -68,6 +112,11 @@ extern "C" {
callback: IOHIDDeviceCallback,
context: *mut c_void,
);
pub fn IOHIDManagerRegisterInputReportCallback(
manager: IOHIDManagerRef,
callback: IOHIDReportCallback,
context: *mut c_void,
);
pub fn IOHIDManagerOpen(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn;
pub fn IOHIDManagerClose(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn;
pub fn IOHIDManagerScheduleWithRunLoop(
@ -84,11 +133,4 @@ extern "C" {
report: *const u8,
reportLength: CFIndex,
) -> IOReturn;
pub fn IOHIDDeviceRegisterInputReportCallback(
device: IOHIDDeviceRef,
report: *const u8,
reportLength: CFIndex,
callback: IOHIDReportCallback,
context: *mut c_void,
);
}

Просмотреть файл

@ -3,30 +3,24 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
extern crate log;
extern crate libc;
use std::thread;
use std::time::Duration;
mod device;
mod devicemap;
mod iokit;
mod iohid;
mod monitor;
use self::devicemap::DeviceMap;
use self::monitor::Monitor;
mod transaction;
use consts::PARAMETER_SIZE;
use khmatcher::KeyHandleMatcher;
use runloop::RunLoop;
use platform::device::Device;
use platform::transaction::Transaction;
use std::thread;
use std::time::Duration;
use u2fprotocol::{u2f_init_device, u2f_register, u2f_sign, u2f_is_keyhandle_valid};
use util::{io_err, OnceCallback};
use u2fprotocol::{u2f_register, u2f_sign, u2f_is_keyhandle_valid};
#[derive(Default)]
pub struct PlatformManager {
// Handle to the thread loop.
thread: Option<RunLoop>,
transaction: Option<Transaction>,
}
impl PlatformManager {
@ -47,56 +41,43 @@ impl PlatformManager {
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);
// Start a new "sign" transaction.
let transaction = Transaction::new(timeout, cbc.clone(), move |device_ref, rx, alive| {
// Create a new device.
let dev = &mut Device::new(device_ref, rx);
'top: while alive() && monitor.alive() {
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 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));
// Try initializing it.
if !u2f_init_device(dev) {
return;
}
// Iterate the exlude 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;
}
// Check to see if monitor.events has any hotplug events that we'll need
// to handle
if monitor.events().size_hint().0 > 0 {
debug!("Hotplug event; restarting loop");
continue 'top;
}
while alive() {
if let Ok(bytes) = u2f_register(dev, &challenge, &application) {
callback.call(Ok(bytes));
break;
}
// Sleep a bit before trying again.
thread::sleep(Duration::from_millis(100));
}
});
callback.call(Err(io_err("aborted or timed out")));
},
timeout,
);
self.thread = Some(try_or!(
thread,
|_| cbc.call(Err(io_err("couldn't create runloop")))
));
// Store the transaction so we can cancel it, if needed.
self.transaction = Some(try_or!(transaction, |_| {
cbc.call(Err(io_err("couldn't create transaction")))
}));
}
pub fn sign(
&mut self,
timeout: u64,
@ -110,85 +91,58 @@ impl PlatformManager {
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);
// Start a new "register" transaction.
let transaction = Transaction::new(timeout, cbc.clone(), move |device_ref, rx, alive| {
// Create a new device.
let dev = &mut Device::new(device_ref, rx);
'top: while alive() && monitor.alive() {
for event in monitor.events() {
devices.process_event(event);
// Try initializing it.
if !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 all devices.
for (path, device) in devices.iter_mut() {
let key_handles = matches.get(path);
// 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<&Vec<u8>>>();
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(device, &blank, &blank) {
if let Ok(_) = u2f_register(dev, &blank, &blank) {
callback.call(Err(io_err("invalid key")));
return;
break;
}
continue;
}
} else {
// Otherwise, try to sign.
for key_handle in key_handles {
if let Ok(bytes) = u2f_sign(
device,
&challenge,
&application,
key_handle,
)
{
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)));
return;
}
}
// Check to see if monitor.events has any hotplug events that we'll
// need to handle
if monitor.events().size_hint().0 > 0 {
debug!("Hotplug event; restarting loop");
continue 'top;
break;
}
}
}
// Sleep a bit before trying again.
thread::sleep(Duration::from_millis(100));
}
});
callback.call(Err(io_err("aborted or timed out")));
},
timeout,
);
self.thread = Some(try_or!(
thread,
|_| cbc.call(Err(io_err("couldn't create runloop")))
));
// Store the transaction so we can cancel it, if needed.
self.transaction = Some(try_or!(transaction, |_| {
cbc.call(Err(io_err("couldn't create transaction")))
}));
}
pub fn cancel(&mut self) {
if let Some(thread) = self.thread.take() {
thread.cancel();
if let Some(mut transaction) = self.transaction.take() {
transaction.cancel();
}
}
}
impl Drop for PlatformManager {
fn drop(&mut self) {
debug!("OSX PlatformManager dropped");
self.cancel();
}
}

Просмотреть файл

@ -2,115 +2,175 @@
* 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::io;
use std::sync::mpsc::{channel, Sender, Receiver, TryIter};
use std::thread;
use super::iohid::*;
use super::iokit::*;
use core_foundation_sys::runloop::*;
use runloop::RunLoop;
extern crate log;
extern crate libc;
use core_foundation_sys::base::*;
use core_foundation_sys::runloop::*;
use libc::c_void;
use platform::iohid::*;
use platform::iokit::*;
use runloop::RunLoop;
use std::{io, slice};
use std::collections::HashMap;
use std::sync::mpsc::{channel, Receiver, Sender};
use util::io_err;
pub enum Event {
Add(IOHIDDeviceRef),
Remove(IOHIDDeviceRef),
struct DeviceData {
tx: Sender<Vec<u8>>,
runloop: RunLoop,
}
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(IOHIDDeviceRef, Receiver<Vec<u8>>, &Fn() -> bool) + Sync,
{
manager: IOHIDManagerRef,
// Keep alive until the monitor goes away.
_matcher: IOHIDDeviceMatcher,
map: HashMap<IOHIDDeviceRef, DeviceData>,
new_device_cb: F,
}
impl Monitor {
pub fn new() -> io::Result<Self> {
let (tx, rx) = channel();
impl<F> Monitor<F>
where
F: Fn(IOHIDDeviceRef, Receiver<Vec<u8>>, &Fn() -> bool) + Sync + 'static,
{
pub fn new(new_device_cb: F) -> Self {
let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) };
let thread = RunLoop::new(move |alive| -> io::Result<()> {
let tx_box = Box::new(tx);
let tx_ptr = Box::into_raw(tx_box) as *mut libc::c_void;
// Match FIDO devices only.
let _matcher = IOHIDDeviceMatcher::new();
unsafe { IOHIDManagerSetDeviceMatching(manager, _matcher.get()) };
// This will keep `tx` alive only for the scope.
let _tx = unsafe { Box::from_raw(tx_ptr as *mut Sender<Event>) };
Self {
manager,
_matcher,
new_device_cb,
map: HashMap::new(),
}
}
// Create and initialize a scoped HID manager.
let manager = IOHIDManager::new()?;
pub fn start(&mut self) -> io::Result<()> {
let context = self as *mut Self as *mut c_void;
// Match only U2F devices.
let dict = IOHIDDeviceMatcher::new();
unsafe { IOHIDManagerSetDeviceMatching(manager.get(), dict.get()) };
// Register callbacks.
unsafe {
IOHIDManagerRegisterDeviceMatchingCallback(
manager.get(),
Monitor::device_add_cb,
tx_ptr,
self.manager,
Monitor::<F>::on_device_matching,
context,
);
IOHIDManagerRegisterDeviceRemovalCallback(
manager.get(),
Monitor::device_remove_cb,
tx_ptr,
self.manager,
Monitor::<F>::on_device_removal,
context,
);
IOHIDManagerRegisterInputReportCallback(
self.manager,
Monitor::<F>::on_input_report,
context,
);
}
// Run the Event Loop. CFRunLoopRunInMode() will dispatch HID
// input reports into the various callbacks
while alive() {
trace!("OSX Runloop running, handle={:?}", thread::current());
if unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, 0) } ==
kCFRunLoopRunStopped
{
debug!("OSX Runloop device stopped.");
break;
}
}
debug!("OSX Runloop completed, handle={:?}", thread::current());
IOHIDManagerScheduleWithRunLoop(
self.manager,
CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode,
);
let rv = IOHIDManagerOpen(self.manager, kIOHIDManagerOptionNone);
if rv == 0 {
Ok(())
})?;
Ok(Self { rx, thread })
} else {
Err(io_err(&format!("Couldn't open HID Manager, rv={}", rv)))
}
}
}
pub fn events(&self) -> TryIter<Event> {
self.rx.try_iter()
pub fn stop(&mut self) {
// Remove all devices.
while !self.map.is_empty() {
let device_ref = *self.map.keys().next().unwrap();
self.remove_device(&device_ref);
}
pub fn alive(&self) -> bool {
self.thread.alive()
// Close the manager and its devices.
unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) };
}
extern "C" fn device_add_cb(
fn remove_device(&mut self, device_ref: &IOHIDDeviceRef) {
if let Some(DeviceData { tx, runloop }) = self.map.remove(device_ref) {
// Dropping `tx` will make Device::read() fail eventually.
drop(tx);
// Wait until the runloop stopped.
runloop.cancel();
}
}
extern "C" fn on_input_report(
context: *mut c_void,
_: IOReturn,
device_ref: IOHIDDeviceRef,
_: IOHIDReportType,
_: u32,
report: *mut u8,
report_len: CFIndex,
) {
let this = unsafe { &mut *(context as *mut Self) };
let mut send_failed = false;
// Ignore the report if we can't find a device for it.
if let Some(&DeviceData { ref tx, .. }) = this.map.get(&device_ref) {
let data = unsafe { slice::from_raw_parts(report, report_len as usize).to_vec() };
send_failed = tx.send(data).is_err();
}
// Remove the device if sending fails.
if send_failed {
this.remove_device(&device_ref);
}
}
extern "C" fn on_device_matching(
context: *mut c_void,
_: IOReturn,
_: *mut c_void,
device: IOHIDDeviceRef,
device_ref: IOHIDDeviceRef,
) {
let tx = unsafe { &*(context as *mut Sender<Event>) };
let _ = tx.send(Event::Add(device));
let this = unsafe { &mut *(context as *mut Self) };
let (tx, rx) = channel();
let f = &this.new_device_cb;
// Create a new per-device runloop.
let runloop = RunLoop::new(move |alive| {
// Ensure that the runloop is still alive.
if alive() {
f(device_ref, rx, alive);
}
});
if let Ok(runloop) = runloop {
this.map.insert(device_ref, DeviceData { tx, runloop });
}
}
extern "C" fn device_remove_cb(
extern "C" fn on_device_removal(
context: *mut c_void,
_: IOReturn,
_: *mut c_void,
device: IOHIDDeviceRef,
device_ref: IOHIDDeviceRef,
) {
let tx = unsafe { &*(context as *mut Sender<Event>) };
let _ = tx.send(Event::Remove(device));
let this = unsafe { &mut *(context as *mut Self) };
this.remove_device(&device_ref);
}
}
impl Drop for Monitor {
impl<F> Drop for Monitor<F>
where
F: Fn(IOHIDDeviceRef, Receiver<Vec<u8>>, &Fn() -> bool) + Sync,
{
fn drop(&mut self) {
debug!("OSX Runloop dropped");
self.thread.cancel();
unsafe { CFRelease(self.manager as *mut libc::c_void) };
}
}

Просмотреть файл

@ -0,0 +1,83 @@
/* 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/. */
extern crate libc;
use core_foundation_sys::runloop::*;
use libc::c_void;
use platform::iohid::CFRunLoopEntryObserver;
use platform::iokit::{IOHIDDeviceRef, SendableRunLoop};
use platform::monitor::Monitor;
use std::io;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use util::{io_err, to_io_err, OnceCallback};
// A transaction will run the given closure in a new thread, thereby using a
// separate per-thread state machine for each HID. It will either complete or
// fail through user action, timeout, or be cancelled when overridden by a new
// transaction.
pub struct Transaction {
runloop: SendableRunLoop,
thread: Option<thread::JoinHandle<()>>,
}
impl Transaction {
pub fn new<F, T>(timeout: u64, callback: OnceCallback<T>, new_device_cb: F) -> io::Result<Self>
where
F: Fn(IOHIDDeviceRef, Receiver<Vec<u8>>, &Fn() -> bool) + Sync + Send + 'static,
T: 'static,
{
let (tx, rx) = channel();
let cbc = callback.clone();
let timeout = (timeout as f64) / 1000.0;
let builder = thread::Builder::new();
let thread = builder.spawn(move || {
// Add a runloop observer that will be notified when we enter the
// runloop and tx.send() the current runloop to the owning thread.
// We need to ensure the runloop was entered before unblocking
// Transaction::new(), so we can always properly cancel.
let context = &tx as *const _ as *mut c_void;
let obs = CFRunLoopEntryObserver::new(Transaction::observe, context);
obs.add_to_current_runloop();
// Create a new HID device monitor and start polling.
let mut monitor = Monitor::new(new_device_cb);
try_or!(monitor.start(), |e| cbc.call(Err(e)));
// This will block until completion, abortion, or timeout.
unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, 0) };
// Close the monitor and its devices.
monitor.stop();
// Send an error, if the callback wasn't called already.
cbc.call(Err(io_err("aborted or timed out")));
})?;
// Block until we enter the CFRunLoop.
let runloop = rx.recv().map_err(to_io_err)?;
Ok(Self {
runloop,
thread: Some(thread),
})
}
extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) {
let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) };
// Send the current runloop to the receiver to unblock it.
let _ = tx.send(SendableRunLoop(unsafe { CFRunLoopGetCurrent() }));
}
pub fn cancel(&mut self) {
// (This call doesn't block.)
unsafe { CFRunLoopStop(*self.runloop) };
// This must never be None. Ignore return value.
let _ = self.thread.take().unwrap().join();
}
}