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}