pcsc-mon 0.1.1

Monitor PC/SC smart card readers with hotplug and card event support
Documentation
use once_cell::sync::Lazy;
use pcsc::{Card, Context, Error, ReaderState, Scope, State};
use std::sync::Mutex;
use std::{
    ffi::CString,
    sync::{atomic::AtomicBool, Arc},
    time::Duration,
};

static PCSC_MONITOR: Lazy<Mutex<PcscMonitor>> = Lazy::new(|| Mutex::new(PcscMonitor::new()));

/// Represents a PC/SC monitor that tracks reader and card state changes.
///
/// `PcscMonitor` is a singleton interface for monitoring smart card readers
/// using PC/SC. It supports hotplug detection, card insertion/removal events,
/// and thread-safe callback registration.
///
/// The monitor runs in a background thread once started.

pub struct PcscMonitor {
    on_reader_added: Option<Arc<dyn Fn(String) + Send + Sync>>,
    on_reader_removed: Option<Arc<dyn Fn(String) + Send + Sync>>,
    on_card_inserted: Option<Arc<dyn Fn(&Context, &Card) + Send + Sync>>,
    on_card_removed: Option<Arc<dyn Fn(String) + Send + Sync>>,
    on_error: Option<Arc<dyn Fn(Error) + Send + Sync>>,
    known_readers: Arc<Mutex<Vec<String>>>,
    reader_states: Arc<Mutex<Vec<(String, State)>>>,
    started: AtomicBool,
}

impl PcscMonitor {
    /// Gets a global instance of the `PcscMonitor` to register callbacks.
    ///
    /// This method returns a locked [`MutexGuard`] to the global monitor instance.
    /// Use this to attach listeners and start monitoring.
    ///
    /// # Example
    /// ```rust
    /// let mut monitor = pcsc_mon::PcscMonitor::instance();
    /// monitor.on_reader_added(|reader| {
    ///     println!("Reader added: {}", reader);
    /// });
    /// monitor.start();
    /// ```
    pub fn instance() -> std::sync::MutexGuard<'static, PcscMonitor> {
        PCSC_MONITOR.lock().expect("PcscMonitor mutex poisoned")
    }
    fn new() -> Self {
        Self {
            // init other fields...
            started: AtomicBool::new(false),
            on_reader_added: None,
            on_reader_removed: None,
            on_card_inserted: None,
            on_card_removed: None,
            on_error: None,
            known_readers: Arc::new(Mutex::new(Vec::new())),
            reader_states: Arc::new(Mutex::new(Vec::new())),
        }
    }
    /// Registers a callback for reader addition events.
    ///
    /// The callback is called with the name of the reader when a new reader is connected.
    pub fn on_reader_added<F>(&mut self, f: F)
    where
        F: Fn(String) + Send + Sync + 'static,
    {
        self.on_reader_added = Some(Arc::new(f));
    }

    /// Registers a callback for reader removal events.
    ///
    /// The callback is called with the name of the reader when a reader is disconnected.
    ///
    /// **Note:** Internally sets the reader state to [`State::IGNORE`]. When the same
    /// reader is reconnected, a card must be inserted and removed again to re-trigger
    /// `on_card_inserted`.
    pub fn on_reader_removed<F>(&mut self, f: F)
    where
        F: Fn(String) + Send + Sync + 'static,
    {
        self.on_reader_removed = Some(Arc::new(f));
    }
    /// Registers a callback for card insertion events.
    ///
    /// The callback receives a reference to the [`Context`] and [`Card`] for direct interaction
    /// with the smart card.
    ///
    /// # Note
    /// The context and card are already connected when the callback runs.
    pub fn on_card_inserted<F>(&mut self, f: F)
    where
        F: Fn(&Context, &Card) + Send + Sync + 'static,
    {
        self.on_card_inserted = Some(Arc::new(f));
    }
    /// Registers a callback for card removal events.
    ///
    /// The callback is called with the name of the reader when the card is removed.
    pub fn on_card_removed<F>(&mut self, f: F)
    where
        F: Fn(String) + Send + Sync + 'static,
    {
        self.on_card_removed = Some(Arc::new(f));
    }

    /// Registers a callback to handle Errors during the pcsc events
    ///
    /// The callback is called with the error thrown during reader or card state detection
    pub fn on_error<F>(&mut self, f: F)
    where
        F: Fn(Error) + Send + Sync + 'static,
    {
        self.on_error = Some(Arc::new(f));
    }

    /// Starts the monitoring thread.
    ///
    /// Begins polling for reader and card state changes. Should be called after all
    /// desired callbacks are registered.
    ///
    /// This is non-blocking: the monitoring thread runs in the background.

    pub fn start(&mut self) {
        if self.started.swap(true, std::sync::atomic::Ordering::SeqCst) {
            // Already started
            return;
        }
        // Establish PC/SC context

        // Clone callbacks for reader thread
        let on_reader_added = self.on_reader_added.clone();
        let on_reader_removed = self.on_reader_removed.clone();
        let on_error_read = self.on_error.clone();
        let known_readers_mutex = self.known_readers.clone();
        println!("known readers mutex: {:?}", known_readers_mutex);
        let reader_states_mutex = self.reader_states.clone();
        println!(
            "reader states mutex (list readers thread): {:?}",
            reader_states_mutex
        );
        std::thread::spawn(move || {
            match Context::establish(Scope::User) {
                Ok(ctx) => {
                    loop {
                        let mut buf = [0u8; 2048];
                        match ctx.list_readers(&mut buf) {
                            Ok(readers_raw) => {
                                let readers = readers_raw
                                    .map(|r| r.to_string_lossy().into_owned())
                                    .collect::<Vec<_>>();

                                let mut known_readers = known_readers_mutex.lock().unwrap();
                                let mut reader_states = reader_states_mutex.lock().unwrap();
                                // Detect added readers
                                for r in readers.iter().filter(|r| !known_readers.contains(r)) {
                                    reader_states.push((r.clone(), State::UNAWARE));
                                    if let Some(ref cb) = on_reader_added {
                                        cb(r.clone());
                                    }
                                }

                                // Detect removed readers
                                for r in known_readers.iter().filter(|r| !readers.contains(r)) {
                                    if let Some(position) =
                                        reader_states.iter().position(|(name, _)| name == r)
                                    {
                                        reader_states.remove(position);
                                    }
                                    if let Some(ref cb) = on_reader_removed {
                                        cb(r.clone());
                                    }
                                }

                                *known_readers = readers;
                            }
                            Err(e) => {
                                if let Some(ref cb) = on_error_read {
                                    cb(e);
                                } else {
                                    eprintln!("Reader listing error: {:?}", e)
                                }
                            }
                        }
                        std::thread::sleep(std::time::Duration::from_secs(1));
                    }
                }
                Err(e) => {
                    if let Some(ref cb) = on_error_read {
                        cb(e);
                    } else {
                        eprintln!("Reader listing error: {:?}", e)
                    }
                }
            }
        });

        // Clone callbacks for card event thread
        let on_card_inserted = self.on_card_inserted.clone();
        let on_card_removed = self.on_card_removed.clone();
        let on_error_card = self.on_error.clone();
        let reader_states_mutex = self.reader_states.clone();
        println!(
            "reader states mutex (reader state thread): {:?}",
            reader_states_mutex
        );
        std::thread::spawn(move || {
            match Context::establish(Scope::User) {
                Ok(ctx) => {
                    loop {
                        let mut reader_states = reader_states_mutex.lock().unwrap();

                        let mut reader_states_structs: Vec<ReaderState> = reader_states
                            .iter()
                            .map(|(name, state)| {
                                ReaderState::new(
                                    CString::new(name.as_str()).expect("CString::new failed"),
                                    *state,
                                )
                            })
                            .collect();

                        match ctx.get_status_change(None, &mut reader_states_structs) {
                            Ok(_) => {
                                for (idx, state) in reader_states_structs.iter().enumerate() {
                                    let reader_name = &reader_states[idx].0;
                                    let event_state = state.event_state();
                                    let current_state = state.current_state();
                                    let changed = current_state == State::UNAWARE
                                        || !event_state.contains(current_state);

                                    // TODO: Check if event state update can be forced after reader is reattached. State goes to ignore until card is inserted and removed again

                                    if !changed {
                                        continue;
                                    }
                                    println!("state changed to {:?}", event_state);
                                    if event_state.contains(State::PRESENT)
                                        && !event_state.contains(State::INUSE)
                                    {
                                        if let Some(ref cb) = on_card_inserted {
                                            match ctx.connect(
                                                state.name(),
                                                pcsc::ShareMode::Shared,
                                                pcsc::Protocols::ANY,
                                            ) {
                                                Ok(card) => {
                                                    println!("card connected");
                                                    cb(&ctx, &card);
                                                }
                                                Err(e) => {
                                                    if let Some(ref cb) = on_error_card {
                                                        cb(e);
                                                    } else {
                                                        eprintln!(
                                                            "failed to connect to card: {:?}",
                                                            e
                                                        )
                                                    }
                                                }
                                            }
                                        }
                                    } else if event_state.contains(State::EMPTY) {
                                        if let Some(ref cb) = on_card_removed {
                                            cb(reader_name.clone());
                                        }
                                    }

                                    reader_states[idx].1 = event_state - State::CHANGED;
                                }
                            }
                            Err(e) => {
                                if let Some(ref cb) = on_error_card {
                                    cb(e);
                                } else {
                                    eprintln!("get_status_change error: {:?}", e)
                                }
                            }
                        }

                        std::thread::sleep(Duration::from_millis(100));
                    }
                }
                Err(e) => {
                    if let Some(ref cb) = on_error_card {
                        cb(e);
                    } else {
                        eprintln!("get_status_change error: {:?}", e)
                    }
                }
            }
            // Track reader states
        });
    }
}