zbus backend#6
Conversation
|
@TitikshaBansal |
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
- added tests, documentation and examples for the zbus backend Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev> diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2490a08..80dd49c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,15 +44,15 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Build (all targets) - run: cargo build --all-targets --verbose + run: cargo build --all-targets --all-features --verbose - - name: Run tests + - name: Run unit tests env: LD_LIBRARY_PATH: /usr/lib:/usr/local/lib - run: cargo test --all --verbose -- --nocapture + run: cargo test --all-targets --all-features --verbose - name: Build examples - run: cargo build --examples --verbose + run: cargo build --examples --all-features --verbose docs: name: Documentation @@ -78,7 +78,7 @@ jobs: - name: Docs env: RUSTDOCFLAGS: -D warnings - run: cargo doc --no-deps -p cpdb-rs + run: cargo doc --no-deps -p cpdb-rs --all-features audit: name: Security Audit @@ -126,6 +126,6 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Build Rust library (bindgen + compile, no link) - run: cargo build --lib --verbose + run: cargo build --lib --all-features --verbose
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
- improve get_all_printers error handling Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
|
@TitikshaBansal |
TitikshaBansal
left a comment
There was a problem hiding this comment.
Thanks for this, @bakayu — and sorry for the delayed review. This is a strong
direction: dropping GLib for native async zbus is a real upgrade, and the effort
shows (clean FFI safety docs, #[non_exhaustive] error enum, the docs.rs
stub-bindings handling in build.rs, the owned/borrowed split, and gating FFI
behind an optional feature — all good calls).
I've left inline comments. Grouping them by priority below so it's easy to
triage. A few blockers, then some things worth addressing before we call this an
industrial release, then minor nits.
Blockers (let's resolve before merge):
-
client.rs~L162 usesstd::thread::sleepinside an async fn. This blocks
the executor thread — on a current-thread runtime the whole app freezes for
200ms. Since the PR's goal is non-blocking async, this one's worth fixing
first:tokio::time::sleep(...).await. -
The README's "automatic keep-alive in the background" — from what I can tell
keep_alive_all()is fully manual (its own doc says to call it periodically),
with no spawned task. Could we either spawn a real stream-bound keep-alive
task, or adjust the README so it matches the current behavior? -
Similarly, "auto-retry handles activation races automatically" — the retry
looks one-shot and only inget_all_printers; the other methods
(get_filtered_printers,get_printer_details,print_fd, etc.) hit the
same race with no retry. Either generalize it or soften the wording.
Worth addressing before an industrial release:
-
links = "cpdb"is unconditional, but the default build is pure-Rust (no FFI).
linksreserves the name globally and can't be feature-gated, so a pure-Rust
default could conflict with a realcpdb-sys. Splitting FFI into a separate
cpdb-syscrate would be the clean fix. -
The library writes recoverable errors to stderr via
eprintln!in several
places. Sincelogis already a dependency,log::warn!/error!would let
consumers control output. -
proxy_for(~L447) matches withends_with, so"PS"resolves to
"...Backend.CUPS". Matching the last.-segment exactly would avoid
ambiguity once there are multiple backends. -
The printer list is decoded by hand from
OwnedValue(unpack_printer_variants
/field_as_str), which silently drops structs on a field mismatch. Since the
proxy already has typed structs (RawOption,RawMedia), a typedRawPrinter
would make this much more robust — this is the most fragile path right now. -
discovery_stream+ backend auto-exit (~30s) + manual keep-alive means a
consumer just awaiting the stream silently stops getting events. Worth coupling
keep-alive to the stream's lifetime, or at minimum a loud doc warning.
Minor / nits:
- A couple of
examples/are named like tests;test_print_jobactually submits
a real job, so acargo run --examplewould have a side effect. Maybe rename to
discover_printers/print_a_document? _connectionfield looks dead — zbus proxies hold their ownArc<Connection>.- This is a sweeping breaking change (default feature flip, FFI now behind
features = ["ffi"]) but still 0.1.0 — a short migration note for existing
cpdb_rs::Frontendusers would help. - Doc typo in
get_printer_details: "in a D-Bus call" → "in a single D-Bus call". - Magic numbers (200ms retry, ~30s auto-exit) could be named consts.
On tests: the count is great, but most of the zbus tests that exercise real logic
are #[ignore] (need a live bus), so the decode path, retry logic, and
proxy_for matching aren't covered in CI. If field_as_str / unpack_printer_variants
were made testable, the suffix-matching bug in (6) would be a nice unit test.
Really good work overall — happy to talk through any of these. Once the blockers
are in I think this is close.
| // If we get an `UnknownMethod` error on the very first call, wait and retry. | ||
| if let Err(zbus::Error::MethodError(ref name, _, _)) = result { | ||
| if name.as_str() == "org.freedesktop.DBus.Error.UnknownMethod" { | ||
| std::thread::sleep(std::time::Duration::from_millis(200)); |
There was a problem hiding this comment.
std::thread::sleep inside an async fn blocks the executor thread (200ms freeze on a current-thread runtime). Could we switch this to tokio::time::sleep(...).await?
| fn proxy_for(&self, backend: &str) -> Result<&PrintBackendProxy<'static>> { | ||
| self.backends | ||
| .iter() | ||
| .find(|b| b.service_name.ends_with(backend) || b.service_name == backend) |
There was a problem hiding this comment.
ends_with matches partial names — e.g. "PS" would resolve to ...Backend.CUPS.
Matching the final .-separated segment exactly would prevent ambiguity with multiple backends.
| } | ||
| } | ||
|
|
||
| /// Extracts a string from a zbus `Value`, returning an empty string on mismatch. |
There was a problem hiding this comment.
This hand-rolled decode silently drops a printer on any field/type mismatch. Since the proxy already has typed structs, deriving a RawPrinter for the (sssssbss) signature would give a real typed error instead of a vanished printer.
| /// ``` | ||
| #[derive(Clone)] | ||
| pub struct CpdbClient { | ||
| _connection: zbus::Connection, |
There was a problem hiding this comment.
This field looks dead — zbus proxies already hold their own Arc<Connection>, so storing _connection to "keep it alive" is redundant. Probably safe to remove (or a quick comment explaining why it's kept).
| ); | ||
| } | ||
|
|
||
| for bh in &self.backends { |
There was a problem hiding this comment.
The errors from do_listing(true) are swallowed with let _, and backends auto-exit after ~30s. Combined with keep-alive being manual, a consumer that just awaits this stream will silently stop receiving events after ~30s with no error. Could we tie a keep-alive task to the stream's lifetime, or at least document this loudly?
| fn print_fd( | ||
| &self, | ||
| printer_id: &str, | ||
| num_settings: i32, |
There was a problem hiding this comment.
num_settings mirrors the C API, but a D-Bus array already carries its own length, so the wrapper could compute this internally rather than asking the caller for it.
There was a problem hiding this comment.
These functions were generated via zbus-xmlgen. If we change the function definition the D-Bus serialization would fail.
However, the public facing function of print_fd already handles internally calculating the array lenght:
https://github.com/bakayu/cpdb-rs/blob/55014a666b8f188d1dae5cfc8122080c53c575f6/src/client.rs#L397
There was a problem hiding this comment.
links = "cpdb" is unconditional, but the default build is now pure-Rust with no C linking. links reserves the name globally for the whole dependency graph and can't be feature-gated, so a pure-Rust default crate would conflict with a real cpdb-sys. Splitting the FFI into a separate cpdb-sys crate would resolve this cleanly, can you please do it also?!
| glib-sys = "0.22" | ||
| glib-sys = { version = "0.22", optional = true } | ||
| serde = { version = "1.0", features = ["derive"] } | ||
| zbus = { version = "5.15.0", default-features = false, features = [ |
There was a problem hiding this comment.
zbus is pulled in with only the tokio feature, so the "native async" client is tokio-only (async-std users are out). Reasonable choice, but worth documenting as a limitation — ideally the runtime feature could be selectable.
|
|
||
| // Spawn a background task to keep the backends from automatically | ||
| // timing out and exiting after 30 seconds of inactivity. | ||
| let keep_alive_client = client.clone(); |
There was a problem hiding this comment.
This describes automatic background keep-alive, but keep_alive_all() looks fully manual (its own doc says to call it periodically) with no spawned task. Could we either spawn a real stream-bound keep-alive task, or reword this so it matches the current behavior?
| - **Pure Rust D-Bus Client:** No `libcpdb-dev` or C compiler needed! Everything runs natively over D-Bus using `zbus`. | ||
| - **Async First:** All methods are `async` and powered by Tokio. | ||
| - **Live Discovery:** Subscribe to a native Rust `Stream` for real-time printer additions, removals, and state changes. | ||
| - **Robust Auto-Retry:** Handles D-Bus activation race conditions automatically. |
There was a problem hiding this comment.
The retry here is one-shot, uses a blocking sleep, and only covers get_all_printers — but get_filtered_printers, get_printer_details, get_translations, print_fd, and print_socket hit the same activation race with no retry. Either generalizing it or softening "robust/automatic" would make the docs match the code.
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
|
@TitikshaBansal I covered all the feedback points, Please review the PR again and let me know if I missed anything. |
|
Also, I was just wondering, why do we have a random rustup-init.exe https://github.com/OpenPrinting/cpdb-rs/blob/bb70758c0a75a691da1c7f604521c15ff6e6595f/rustup-init.exe in the master branch? |
TitikshaBansal
left a comment
There was a problem hiding this comment.
This is a really strong iteration, @bakayu — thanks for working through all of it.
Nearly everything from the last round is properly addressed: the blocking sleep is
gone (and retry now covers all methods, not just get_all_printers), the README
claims match the code now, FFI is cleanly split into a cpdb-sys workspace crate,
eprintln! → log, proxy_for does exact segment matching, and the printer
decode is a typed RawPrinter now. Turning DiscoveryStream into a real type that
cancels keep-alive on Drop is especially clean.
The restructure did introduce two new blockers, plus a couple of things tied to
them. Once these are in I think we're basically there.
Blockers:
-
The
ffi-only build doesn't compile.config/events/mediaare gated
behindzbus-backend, but theirpub usere-exports inlib.rsare
unconditional, socargo build --no-default-features --features ffifails with
unresolved imports — and that's exactly the migration command the README gives
legacy users. Adding#[cfg(feature = "zbus-backend")]to those three re-exports
(likeclient::CpdbClientalready has) should fix it. -
rustup-init.exegot committed to the repo. It would ship to crates.io on
publish — worth agit rmand a.gitignoreentry.
Worth doing alongside these:
-
tokiois a non-optional dependency with no features declared, but the code now
usestokio::spawn(needsrt) andtokio::time::sleep(needstime) directly.
It compiles today only because zbus transitively enables them — if that ever
changes, or someone builds withoutzbus-backend, it breaks. Declaring the
features we use directly (and making it optional + part of thezbus-backend
feature) would be the robust fix, and it'd stop ffi-only consumers from pulling
in the whole tokio tree. -
The CI only runs
--all-features, which is why (1) slipped through — with all
features on, the gated modules exist. A small feature matrix (ffi-only,
zbus-backend-only) would catch this class of issue.cargo-hack --feature-powersetdoes it in one line.
Cleanup before publish:
include/wrapper.hat the root is now an orphan —build.rsmoved to
cpdb-sys/and usescpdb-sys/include/wrapper.h. The root copy is an unreferenced
duplicate that would still ship.PRODUCTION_READINESS.mdnow contradicts HEAD (it says thecpdb-syssplit was
deferred, but this PR did it; paths point to the old layout). Since it's an
internal planning doc it'd also ship on publish — either update it or add it to
Cargo.tomlexclude.- Publishing order:
cpdb-rsnow depends oncpdb-sysvia a path+version dep, so
cpdb-syshas to be published to crates.io first. It's also missingrepository
/readme/rust-version. Worth documenting the release order.
Minor:
examples/zbus_test.rs— the rename pass missed this one (the others got
filter_printers/print_a_documentetc.); something likediscover_printers
would match.discovery_stream(~L323) still swallows thedo_listingerror withlet _, so
a failed initial listing returnsOk(stream)and the consumer silently gets an
empty stream. A per-backendlog::warn!would help.retry_dbus!wrappingprint_fd/print_socketis safe (retry only fires on
UnknownMethod, i.e. no job was created), but that idempotency assumption is
non-obvious — worth a one-line comment.- Multiple
discovery_stream()calls each spawn their own keep-alive task pinging
all backends; might be worth deduping or documenting. - Still 0.1.0 for a breaking re-arch (carryover).
Great work overall — happy to pair on the ffi-only fix if useful.
| #[cfg(feature = "zbus-backend")] | ||
| pub mod proxy; | ||
|
|
||
| // Re-export core types for convenience. |
There was a problem hiding this comment.
These three modules are gated behind zbus-backend, but their pub use re-exports just below are unconditional, so cargo build --no-default-features --features ffi fails with unresolved import config/events/media. That's the same command the README hands legacy users for migration. Adding #[cfg(feature = "zbus-backend")] to these re-exports (matching client::CpdbClient on L62) should
resolve it.
| printer_id: &str, | ||
| backend: &str, | ||
| settings: &[(&str, &str)], | ||
| title: &str, |
There was a problem hiding this comment.
Wrapping print_fd / print_socket in retry_dbus! is safe since retry only fires
on UnknownMethod (no job was created), but that idempotency reasoning isn't
obvious to a future reader — worth a short comment on the macro or call sites. Minor:
the macro evaluates $method($args) twice; fine here since args are side-effect-free,
but noting for macro hygiene.
| } | ||
|
|
||
| for bh in &self.backends { | ||
| let _ = bh.proxy.do_listing(true).await; |
There was a problem hiding this comment.
Keep-alive actually spawning now is great. But if this initial do_listing fails,
discovery_stream still returns Ok(stream) and the consumer gets a silent empty
stream. A per-backend log::warn! here would surface it.
| ], optional = true } | ||
| futures-util = "0.3" | ||
| tokio = "1" | ||
| cpdb-sys = { path = "cpdb-sys", version = "0.1.0", optional = true } |
There was a problem hiding this comment.
tokio is non-optional with no features, but the code now uses tokio::spawn
(rt) and tokio::time::sleep (time) directly — it only compiles because zbus
enables those transitively. Cargo's guidance is to declare features you use
directly. Suggest:
tokio = { version = "1", features = ["rt", "time"], optional = true }
and adding "dep:tokio" to the zbus-backend feature. That also keeps ffi-only
consumers from pulling in the full tokio tree.
You are right. It must have been added by mistake. Can you also add this file in .gitignore and remove it as a part of your PR? |
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
|
@TitikshaBansal the PR is ready for review again! all the feedback has been addressed. |
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
| CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} | ||
| LD_LIBRARY_PATH: /usr/lib:/usr/local/lib | ||
| run: cargo publish --verbose | ||
| run: cargo publish -p cpdb-sys --verbose |
There was a problem hiding this comment.
This publishes cpdb-sys on every tagged release with no "skip if exists" guard. On
a release where only cpdb-rs changed, this step hard-fails with "version already
exists" and stops the whole release (the retry loop only covers cpdb-rs). Either
guard it on the index, e.g.:
if cargo search cpdb-sys | grep -q "cpdb-sys = \"$VER\""; then
echo "cpdb-sys $VER already published, skipping"
else
cargo publish -p cpdb-sys
fi
or commit to always bumping both crates in lockstep — in which case the tag check
(~L58-65) should validate both Cargo.tomls, not just the root.
There was a problem hiding this comment.
Ok, so for this I just pushed a change, I added a similar guard on cpdb-rs as well. With this we can release either or both through the workflow now.
| license = "MIT" | ||
| keywords = ["printing", "cups", "openprinting", "ffi", "cpdb"] | ||
| categories = ["api-bindings", "external-ffi-bindings", "os::unix-apis"] | ||
| readme = "../README.md" |
There was a problem hiding this comment.
readme = "../README.md" means cpdb-sys ships the cpdb-rs README — so its
crates.io/docs.rs front page would say "Pure Rust, no C compiler needed," which is
the opposite of what cpdb-sys is (the FFI layer that needs libcpdb + a C compiler).
A short dedicated cpdb-sys/README.md plus a crate-level //! doc would point users
to cpdb-rs instead.
| @@ -0,0 +1,22 @@ | |||
| [package] | |||
There was a problem hiding this comment.
Minor: missing homepage / documentation here (cpdb-rs has both) — worth adding
for parity before publish.
| .map_err(CpdbError::from)?; | ||
| all.push( | ||
| added | ||
| .filter_map(|sig| async move { |
There was a problem hiding this comment.
sig.args().ok()? drops a malformed signal silently. Fine coming from a typed,
trusted proxy, but a log::debug! on the drop would make future debugging easier.
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
|
@TitikshaBansal addressed the latest feedback with these. Ready for review again! |
Description
This PR introduces a pure native, async Rust client for CPDB using
zbus, completely eliminating the dependency on GLib.The new
zbus-backendprovides a modern async API that allows developers to seamlessly integrate print dialogs without blocking the UI thread. Thecpdb-libsC bindings have been moved behind an optionalffifeature flag.Key Features
CpdbClientwhich automatically discovers and connects to all active CPDB backends.discovery_stream()which yields real-timePrinterAdded,PrinterRemoved, andPrinterStateChangedsignals, making it easy to build reactive print dialog UIs.Note: The new
zbus-backendis now the default feature for the crate.