pcsc_mon/
lib.rs

1use once_cell::sync::Lazy;
2use pcsc::{Card, Context, ReaderState, Scope, State};
3use std::sync::Mutex;
4use std::{
5    ffi::CString,
6    sync::{atomic::AtomicBool, Arc},
7    time::Duration,
8};
9
10static PCSC_MONITOR: Lazy<Mutex<PcscMonitor>> = Lazy::new(|| Mutex::new(PcscMonitor::new()));
11
12/// Represents a PC/SC monitor that tracks reader and card state changes.
13///
14/// `PcscMonitor` is a singleton interface for monitoring smart card readers
15/// using PC/SC. It supports hotplug detection, card insertion/removal events,
16/// and thread-safe callback registration.
17///
18/// The monitor runs in a background thread once started.
19
20pub struct PcscMonitor {
21    on_reader_added: Option<Arc<dyn Fn(String) + Send + Sync>>,
22    on_reader_removed: Option<Arc<dyn Fn(String) + Send + Sync>>,
23    on_card_inserted: Option<Arc<dyn Fn(&Context, &Card) + Send + Sync>>,
24    on_card_removed: Option<Arc<dyn Fn(String) + Send + Sync>>,
25    known_readers: Arc<Mutex<Vec<String>>>,
26    reader_states: Arc<Mutex<Vec<(String, State)>>>,
27    started: AtomicBool,
28}
29
30impl PcscMonitor {
31    /// Gets a global instance of the `PcscMonitor` to register callbacks.
32    ///
33    /// This method returns a locked [`MutexGuard`] to the global monitor instance.
34    /// Use this to attach listeners and start monitoring.
35    ///
36    /// # Example
37    /// ```rust
38    /// let mut monitor = pcsc_mon::PcscMonitor::instance();
39    /// monitor.on_reader_added(|reader| {
40    ///     println!("Reader added: {}", reader);
41    /// });
42    /// monitor.start();
43    /// ```
44    pub fn instance() -> std::sync::MutexGuard<'static, PcscMonitor> {
45        PCSC_MONITOR.lock().expect("PcscMonitor mutex poisoned")
46    }
47    fn new() -> Self {
48        Self {
49            // init other fields...
50            started: AtomicBool::new(false),
51            on_reader_added: None,
52            on_reader_removed: None,
53            on_card_inserted: None,
54            on_card_removed: None,
55            known_readers: Arc::new(Mutex::new(Vec::new())),
56            reader_states: Arc::new(Mutex::new(Vec::new())),
57        }
58    }
59    /// Registers a callback for reader addition events.
60    ///
61    /// The callback is called with the name of the reader when a new reader is connected.
62    pub fn on_reader_added<F>(&mut self, f: F)
63    where
64        F: Fn(String) + Send + Sync + 'static,
65    {
66        self.on_reader_added = Some(Arc::new(f));
67    }
68
69    /// Registers a callback for reader removal events.
70    ///
71    /// The callback is called with the name of the reader when a reader is disconnected.
72    ///
73    /// **Note:** Internally sets the reader state to [`State::IGNORE`]. When the same
74    /// reader is reconnected, a card must be inserted and removed again to re-trigger
75    /// `on_card_inserted`.
76    pub fn on_reader_removed<F>(&mut self, f: F)
77    where
78        F: Fn(String) + Send + Sync + 'static,
79    {
80        self.on_reader_removed = Some(Arc::new(f));
81    }
82    /// Registers a callback for card insertion events.
83    ///
84    /// The callback receives a reference to the [`Context`] and [`Card`] for direct interaction
85    /// with the smart card.
86    ///
87    /// # Note
88    /// The context and card are already connected when the callback runs.
89    pub fn on_card_inserted<F>(&mut self, f: F)
90    where
91        F: Fn(&Context, &Card) + Send + Sync + 'static,
92    {
93        self.on_card_inserted = Some(Arc::new(f));
94    }
95    /// Registers a callback for card removal events.
96    ///
97    /// The callback is called with the name of the reader when the card is removed.
98    pub fn on_card_removed<F>(&mut self, f: F)
99    where
100        F: Fn(String) + Send + Sync + 'static,
101    {
102        self.on_card_removed = Some(Arc::new(f));
103    }
104
105    /// Starts the monitoring thread.
106    ///
107    /// Begins polling for reader and card state changes. Should be called after all
108    /// desired callbacks are registered.
109    ///
110    /// This is non-blocking: the monitoring thread runs in the background.
111
112    pub fn start(&mut self) {
113        if self.started.swap(true, std::sync::atomic::Ordering::SeqCst) {
114            // Already started
115            return;
116        }
117        // Establish PC/SC context
118
119        // Clone callbacks for reader thread
120        let on_reader_added = self.on_reader_added.clone();
121        let on_reader_removed = self.on_reader_removed.clone();
122
123        std::thread::spawn(move || {
124            let ctx = Context::establish(Scope::User).expect("Failed to establish context");
125            let known_readers_mutex = PcscMonitor::instance().known_readers.clone();
126            let reader_states_mutex = PcscMonitor::instance().reader_states.clone();
127
128            loop {
129                let mut buf = [0u8; 2048];
130                match ctx.list_readers(&mut buf) {
131                    Ok(readers_raw) => {
132                        let readers = readers_raw
133                            .map(|r| r.to_string_lossy().into_owned())
134                            .collect::<Vec<_>>();
135
136                        let mut known_readers = known_readers_mutex.lock().unwrap();
137                        let mut reader_states = reader_states_mutex.lock().unwrap();
138                        // Detect added readers
139                        for r in readers.iter().filter(|r| !known_readers.contains(r)) {
140                            reader_states.push((r.clone(), State::UNAWARE));
141                            if let Some(ref cb) = on_reader_added {
142                                cb(r.clone());
143                            }
144                        }
145
146                        // Detect removed readers
147                        for r in known_readers.iter().filter(|r| !readers.contains(r)) {
148                            if let Some(position) =
149                                reader_states.iter().position(|(name, _)| name == r)
150                            {
151                                reader_states.remove(position);
152                            }
153                            if let Some(ref cb) = on_reader_removed {
154                                cb(r.clone());
155                            }
156                        }
157
158                        *known_readers = readers;
159                    }
160                    Err(e) => eprintln!("Reader listing error: {:?}", e),
161                }
162                std::thread::sleep(std::time::Duration::from_secs(1));
163            }
164        });
165
166        // Clone callbacks for card event thread
167        let on_card_inserted = self.on_card_inserted.clone();
168        let on_card_removed = self.on_card_removed.clone();
169
170        std::thread::spawn(move || {
171            let ctx = Context::establish(Scope::User).expect("Failed to establish context");
172            // Track reader states
173            let reader_states_mutex = PcscMonitor::instance().reader_states.clone();
174
175            loop {
176                let mut reader_states = reader_states_mutex.lock().unwrap();
177
178                let mut reader_states_structs: Vec<ReaderState> = reader_states
179                    .iter()
180                    .map(|(name, state)| {
181                        ReaderState::new(
182                            CString::new(name.as_str()).expect("CString::new failed"),
183                            *state,
184                        )
185                    })
186                    .collect();
187
188                match ctx.get_status_change(None, &mut reader_states_structs) {
189                    Ok(_) => {
190                        for (idx, state) in reader_states_structs.iter().enumerate() {
191                            let reader_name = &reader_states[idx].0;
192                            let event_state = state.event_state();
193                            let current_state = state.current_state();
194                            let changed = current_state == State::UNAWARE
195                                || !event_state.contains(current_state);
196
197                            // TODO: Check if event state update can be forced after reader is reattached. State goes to ignore until card is inserted and removed again
198
199                            if !changed {
200                                continue;
201                            }
202                            println!("state changed to {:?}", event_state);
203                            if event_state.contains(State::PRESENT)
204                                && !event_state.contains(State::INUSE)
205                            {
206                                if let Some(ref cb) = on_card_inserted {
207                                    match ctx.connect(
208                                        state.name(),
209                                        pcsc::ShareMode::Shared,
210                                        pcsc::Protocols::ANY,
211                                    ) {
212                                        Ok(card) => {
213                                            println!("card connected");
214                                            cb(&ctx, &card);
215                                        }
216                                        Err(e) => eprintln!("Failed to connect to card: {:?}", e),
217                                    }
218                                }
219                            } else if event_state.contains(State::EMPTY) {
220                                if let Some(ref cb) = on_card_removed {
221                                    cb(reader_name.clone());
222                                }
223                            }
224
225                            reader_states[idx].1 = event_state - State::CHANGED;
226                        }
227                    }
228                    Err(e) => eprintln!("get_status_change error: {:?}", e),
229                }
230
231                std::thread::sleep(Duration::from_millis(100));
232            }
233        });
234    }
235}