cpdb_sys/frontend.rs
1//! Safe wrapper around `cpdb_frontend_obj_t`.
2//!
3//! A [`Frontend`] is the central object for printer discovery. It manages
4//! a D-Bus connection, holds the backend list, and owns the hash table
5//! of discovered printers.
6//!
7//! # Threading
8//!
9//! [`Frontend`] is [`Send`] but **not** [`Sync`]. Most methods take
10//! `&self` for ergonomics but mutate the underlying C state, and
11//! cpdb-libs does not lock internally. If you need concurrent access,
12//! wrap the frontend in a [`std::sync::Mutex`].
13
14use super::bindings as ffi;
15use super::callbacks::{self, PrinterUpdate};
16use super::printer::Printer;
17use crate::error::{CpdbError, Result};
18use std::ffi::{CStr, CString};
19use std::mem::MaybeUninit;
20use std::ptr::NonNull;
21
22/// Safe wrapper around `cpdb_frontend_obj_t`.
23pub struct Frontend {
24 raw: NonNull<ffi::cpdb_frontend_obj_t>,
25}
26
27// SAFETY: `Frontend` owns its `cpdb_frontend_obj_t *`. Moving it across
28// threads is fine; concurrent access from multiple threads is not, so we
29// deliberately omit `Sync`.
30unsafe impl Send for Frontend {}
31
32impl Frontend {
33 // ─── Construction ────────────────────────────────────────────────────────
34
35 /// Creates a new frontend with the default printer-update callback.
36 pub fn new() -> Result<Self> {
37 Self::new_internal(None)
38 }
39
40 /// Creates a new frontend with a raw printer-update callback.
41 ///
42 /// Prefer [`Frontend::new_with_observer`] for closure-based callbacks.
43 /// This entry point exists for callers that need to interoperate with a
44 /// hand-written `extern "C"` callback.
45 ///
46 /// The callback is invoked from the cpdb-libs internal D-Bus listener
47 /// thread when printers are added, removed, or change state. It must be
48 /// thread-safe and must not call back into this `Frontend`.
49 pub fn new_with_callback(cb: ffi::cpdb_printer_callback) -> Result<Self> {
50 Self::new_internal(cb)
51 }
52
53 /// Creates a new frontend with a closure-based printer observer.
54 ///
55 /// The observer is invoked from cpdb-libs' internal D-Bus listener
56 /// thread when printers are added, removed, or change state. Because
57 /// `cpdb_printer_callback` carries no `user_data`, the closure is
58 /// stored in a process-global registry keyed by the frontend pointer
59 /// and removed automatically when the [`Frontend`] is dropped.
60 ///
61 /// The closure must be `Send + 'static`. Panics inside the closure are
62 /// caught by `catch_unwind` and absorbed; do not rely on panic-based
63 /// flow control inside callbacks.
64 pub fn new_with_observer<F>(observer: F) -> Result<Self>
65 where
66 F: FnMut(&Printer<'_>, PrinterUpdate) + Send + 'static,
67 {
68 let cb: ffi::cpdb_printer_callback = Some(callbacks::printer_trampoline);
69 let frontend = Self::new_internal(cb)?;
70 callbacks::register_printer_observer(frontend.raw.as_ptr(), Box::new(observer));
71 Ok(frontend)
72 }
73
74 fn new_internal(cb: ffi::cpdb_printer_callback) -> Result<Self> {
75 // SAFETY: `cpdbGetNewFrontendObj` is a constructor; the callback may
76 // be null.
77 let raw = unsafe { ffi::cpdbGetNewFrontendObj(cb) };
78 NonNull::new(raw)
79 .map(|raw| Self { raw })
80 .ok_or_else(|| CpdbError::FrontendError("cpdbGetNewFrontendObj returned null".into()))
81 }
82
83 /// Wraps an already-allocated frontend object.
84 ///
85 /// # Safety
86 /// `raw` must be a valid pointer to a `cpdb_frontend_obj_t` obtained from
87 /// cpdb-libs, not aliased by any other Rust handle, and not yet freed.
88 /// Ownership transfers to the returned `Frontend`.
89 pub unsafe fn from_raw(raw: *mut ffi::cpdb_frontend_obj_t) -> Result<Self> {
90 NonNull::new(raw)
91 .map(|raw| Self { raw })
92 .ok_or(CpdbError::NullPointer)
93 }
94
95 /// Returns the raw pointer for use within this crate.
96 #[doc(hidden)]
97 pub fn as_raw(&self) -> *mut ffi::cpdb_frontend_obj_t {
98 self.raw.as_ptr()
99 }
100
101 // ─── Lifecycle ───────────────────────────────────────────────────────────
102
103 /// Tells the frontend to ignore the previously saved settings file.
104 ///
105 /// Must be called before [`Frontend::connect_to_dbus`] to take effect.
106 pub fn ignore_last_saved_settings(&self) {
107 // SAFETY: pointer is non-null.
108 unsafe { ffi::cpdbIgnoreLastSavedSettings(self.raw.as_ptr()) };
109 }
110
111 /// Connects to the session D-Bus and activates the print backends.
112 pub fn connect_to_dbus(&self) -> Result<()> {
113 // SAFETY: pointer is non-null.
114 unsafe { ffi::cpdbConnectToDBus(self.raw.as_ptr()) };
115 Ok(())
116 }
117
118 /// Returns `true` when cpdb-libs currently holds a live D-Bus connection.
119 ///
120 /// This consults `cpdbGetDbusConnection`, which is a process-global —
121 /// not per-frontend — query, so the result reflects the overall
122 /// cpdb-libs state rather than this specific [`Frontend`].
123 pub fn dbus_connected() -> bool {
124 // SAFETY: no arguments; returns either a live `GDBusConnection *`
125 // or null.
126 !unsafe { ffi::cpdbGetDbusConnection() }.is_null()
127 }
128
129 /// Disconnects from D-Bus.
130 pub fn disconnect_from_dbus(&self) -> Result<()> {
131 // SAFETY: pointer is non-null.
132 unsafe { ffi::cpdbDisconnectFromDBus(self.raw.as_ptr()) };
133 Ok(())
134 }
135
136 /// Re-activates the backends (rediscovers printers without disconnecting).
137 pub fn activate_backends(&self) {
138 // SAFETY: pointer is non-null.
139 unsafe { ffi::cpdbActivateBackends(self.raw.as_ptr()) };
140 }
141
142 /// Starts the background thread that periodically refreshes the backend list.
143 pub fn start_backend_list_refreshing(&self) {
144 // SAFETY: pointer is non-null.
145 unsafe { ffi::cpdbStartBackendListRefreshing(self.raw.as_ptr()) };
146 }
147
148 /// Stops the backend-list refreshing thread; blocks until it joins.
149 pub fn stop_backend_list_refreshing(&self) {
150 // SAFETY: pointer is non-null.
151 unsafe { ffi::cpdbStopBackendListRefreshing(self.raw.as_ptr()) };
152 }
153
154 /// Starts the printer-listing flow (`cpdbStartListingPrinters`), creating
155 /// a fresh frontend bound to the supplied callback.
156 pub fn start_listing(cb: ffi::cpdb_printer_callback) -> Result<Self> {
157 // SAFETY: callback may be null per upstream docs.
158 let raw = unsafe { ffi::cpdbStartListingPrinters(cb) };
159 NonNull::new(raw).map(|raw| Self { raw }).ok_or_else(|| {
160 CpdbError::FrontendError("cpdbStartListingPrinters returned null".into())
161 })
162 }
163
164 /// Stops the printer-listing flow.
165 pub fn stop_listing_printers(&self) {
166 // SAFETY: pointer is non-null.
167 unsafe { ffi::cpdbStopListingPrinters(self.raw.as_ptr()) };
168 }
169
170 // ─── Visibility toggles ──────────────────────────────────────────────────
171
172 /// Hides remote printers from subsequent listings.
173 pub fn hide_remote_printers(&self) {
174 // SAFETY: pointer is non-null.
175 unsafe { ffi::cpdbHideRemotePrinters(self.raw.as_ptr()) };
176 }
177
178 /// Unhides remote printers.
179 pub fn unhide_remote_printers(&self) {
180 // SAFETY: pointer is non-null.
181 unsafe { ffi::cpdbUnhideRemotePrinters(self.raw.as_ptr()) };
182 }
183
184 /// Hides temporary printers from subsequent listings.
185 pub fn hide_temporary_printers(&self) {
186 // SAFETY: pointer is non-null.
187 unsafe { ffi::cpdbHideTemporaryPrinters(self.raw.as_ptr()) };
188 }
189
190 /// Unhides temporary printers.
191 pub fn unhide_temporary_printers(&self) {
192 // SAFETY: pointer is non-null.
193 unsafe { ffi::cpdbUnhideTemporaryPrinters(self.raw.as_ptr()) };
194 }
195
196 // ─── Lookup ──────────────────────────────────────────────────────────────
197
198 /// Finds a printer by `(id, backend)`.
199 ///
200 /// The returned [`Printer`] borrows from `self`.
201 pub fn find_printer<'f>(&'f self, printer_id: &str, backend_name: &str) -> Result<Printer<'f>> {
202 let c_id = CString::new(printer_id)?;
203 let c_backend = CString::new(backend_name)?;
204 // SAFETY: pointers are non-null; the CStrings outlive the call.
205 let raw = unsafe {
206 ffi::cpdbFindPrinterObj(self.raw.as_ptr(), c_id.as_ptr(), c_backend.as_ptr())
207 };
208 if raw.is_null() {
209 Err(CpdbError::NotFound(format!(
210 "printer '{printer_id}' on backend '{backend_name}'"
211 )))
212 } else {
213 Printer::from_raw_borrowed(raw)
214 }
215 }
216
217 /// Returns the user-default printer, if one is set.
218 pub fn get_default_printer(&self) -> Result<Printer<'_>> {
219 // SAFETY: pointer is non-null.
220 let raw = unsafe { ffi::cpdbGetDefaultPrinter(self.raw.as_ptr()) };
221 if raw.is_null() {
222 Err(CpdbError::NotFound("default printer".into()))
223 } else {
224 Printer::from_raw_borrowed(raw)
225 }
226 }
227
228 /// Returns the default printer for a specific backend, if one is set.
229 pub fn get_default_printer_for_backend(&self, backend_name: &str) -> Result<Printer<'_>> {
230 let c_backend = CString::new(backend_name)?;
231 // SAFETY: pointer is non-null; the CString outlives the call.
232 let raw =
233 unsafe { ffi::cpdbGetDefaultPrinterForBackend(self.raw.as_ptr(), c_backend.as_ptr()) };
234 if raw.is_null() {
235 Err(CpdbError::NotFound(format!(
236 "default printer for backend '{backend_name}'"
237 )))
238 } else {
239 Printer::from_raw_borrowed(raw)
240 }
241 }
242
243 /// Asks every backend to refresh its printer list.
244 pub fn refresh_printers(&self) {
245 // SAFETY: pointer is non-null.
246 unsafe { ffi::cpdbGetAllPrinters(self.raw.as_ptr()) };
247 }
248
249 /// Adds an owned printer to the frontend's table.
250 ///
251 /// Takes ownership of the printer — cpdb-libs becomes responsible for
252 /// freeing it. The argument must be an owned [`Printer`] (e.g. from
253 /// [`Printer::load_from_file`]); attempting to insert a borrowed
254 /// printer is rejected to prevent the same pointer ending up in two
255 /// hash tables.
256 ///
257 /// Returns `Ok(true)` when the printer was inserted; `Ok(false)` when
258 /// it was rejected by the backend.
259 ///
260 /// [`Printer::load_from_file`]: crate::Printer::load_from_file
261 pub fn add_printer(&self, printer: Printer<'static>) -> Result<bool> {
262 if !printer.is_owned() {
263 return Err(CpdbError::PrinterError(
264 "add_printer requires an owned Printer".into(),
265 ));
266 }
267 // SAFETY: ownership of the raw pointer is transferred to cpdb-libs
268 // by forgetting the Rust `Printer` (so its Drop does not run).
269 let raw = printer.as_raw();
270 std::mem::forget(printer);
271 let added = unsafe { ffi::cpdbAddPrinter(self.raw.as_ptr(), raw) };
272 Ok(added != 0)
273 }
274
275 /// Removes a printer from the frontend's table by `(id, backend)`.
276 ///
277 /// Returns the *owned* removed printer when present, so the caller can
278 /// inspect it before dropping (cpdb-libs hands ownership back).
279 pub fn remove_printer(
280 &self,
281 printer_id: &str,
282 backend_name: &str,
283 ) -> Result<Option<Printer<'static>>> {
284 let c_id = CString::new(printer_id)?;
285 let c_backend = CString::new(backend_name)?;
286 // SAFETY: pointers are non-null; the CStrings outlive the call.
287 let raw =
288 unsafe { ffi::cpdbRemovePrinter(self.raw.as_ptr(), c_id.as_ptr(), c_backend.as_ptr()) };
289 if raw.is_null() {
290 Ok(None)
291 } else {
292 Printer::from_raw_owned(raw).map(Some)
293 }
294 }
295
296 /// Asks a specific backend to refresh its printer list. Returns `true` on success.
297 pub fn refresh_printer_list(&self, backend_name: &str) -> Result<bool> {
298 let c_backend = CString::new(backend_name)?;
299 // SAFETY: pointers are non-null; the CString outlives the call.
300 let ok = unsafe { ffi::cpdbRefreshPrinterList(self.raw.as_ptr(), c_backend.as_ptr()) };
301 Ok(ok)
302 }
303
304 /// Returns every printer currently known by walking the internal hash table.
305 ///
306 /// The returned printers borrow from `self`.
307 pub fn get_printers(&self) -> Result<Vec<Printer<'_>>> {
308 // SAFETY: dereferencing the printer table field is sound; we only
309 // read borrowed pointers and never write through them.
310 let table = unsafe { (*self.raw.as_ptr()).printer } as *mut glib_sys::GHashTable;
311 if table.is_null() {
312 return Ok(Vec::new());
313 }
314
315 let mut printers: Vec<Printer<'_>> = Vec::new();
316 // SAFETY: iterator is initialised on the stack and iterated
317 // synchronously; the table is not mutated during this loop.
318 unsafe {
319 let mut iter = MaybeUninit::<glib_sys::GHashTableIter>::uninit();
320 glib_sys::g_hash_table_iter_init(iter.as_mut_ptr(), table);
321 let mut iter = iter.assume_init();
322
323 let mut key: glib_sys::gpointer = std::ptr::null_mut();
324 let mut value: glib_sys::gpointer = std::ptr::null_mut();
325 while glib_sys::g_hash_table_iter_next(&mut iter, &mut key, &mut value) != 0 {
326 let raw = value as *mut ffi::cpdb_printer_obj_t;
327 if let Ok(p) = Printer::from_raw_borrowed(raw) {
328 printers.push(p);
329 }
330 }
331 }
332 Ok(printers)
333 }
334
335 /// Looks up the first printer whose `name` field equals the argument.
336 ///
337 /// When multiple printers share a name across backends, the first one
338 /// encountered during hash-table iteration wins. Prefer
339 /// [`Frontend::find_printer`] when you can supply a backend name.
340 pub fn get_printer<'f>(&'f self, name: &str) -> Result<Printer<'f>> {
341 // SAFETY: dereferencing the printer table field is sound.
342 let table = unsafe { (*self.raw.as_ptr()).printer } as *mut glib_sys::GHashTable;
343 if table.is_null() {
344 return Err(CpdbError::NotFound(format!("printer '{name}'")));
345 }
346 let needle = name.as_bytes();
347
348 // SAFETY: see `get_printers`.
349 unsafe {
350 let mut iter = MaybeUninit::<glib_sys::GHashTableIter>::uninit();
351 glib_sys::g_hash_table_iter_init(iter.as_mut_ptr(), table);
352 let mut iter = iter.assume_init();
353
354 let mut key: glib_sys::gpointer = std::ptr::null_mut();
355 let mut value: glib_sys::gpointer = std::ptr::null_mut();
356 while glib_sys::g_hash_table_iter_next(&mut iter, &mut key, &mut value) != 0 {
357 let raw = value as *mut ffi::cpdb_printer_obj_t;
358 if raw.is_null() {
359 continue;
360 }
361 let name_ptr = (*raw).name;
362 if name_ptr.is_null() {
363 continue;
364 }
365 if CStr::from_ptr(name_ptr).to_bytes() == needle {
366 return Printer::from_raw_borrowed(raw);
367 }
368 }
369 }
370 Err(CpdbError::NotFound(format!("printer '{name}'")))
371 }
372}
373
374impl Drop for Frontend {
375 fn drop(&mut self) {
376 // Unregister any observer FIRST so an in-flight callback from
377 // cpdb-libs' D-Bus thread finds an empty slot and bails out
378 // instead of touching a half-freed object.
379 callbacks::unregister_printer_observer(self.raw.as_ptr());
380 // SAFETY: we own the pointer.
381 unsafe { ffi::cpdbDeleteFrontendObj(self.raw.as_ptr()) };
382 }
383}