From 6c50f0c354666351844729829d6d3915d187bafc Mon Sep 17 00:00:00 2001 From: Tait Hoyem Date: Sun, 1 Sep 2024 13:36:35 -0600 Subject: [PATCH] First attempt at basic axum-style state management (#138) * First attempt at basic axum-style state management Empty service/handler impl I'm starting to understand the basis of Tower's service model. However, that does not make this attempt good. This only covers the case of a function that takes no arguments (and that is on purpose). In order to take more arguments, I will need to create a trait which can handle them, think something like Axum's `FromRequest`. We will need an adapted form of this, since we actually will want it for `FromEvent`. You might be wondering why we don't just use `From`, but that is annoyingly problematic, since you will get conflicting implementations because "downstream crates may implement this for ..."; as far as I can tell, this will require a custom trait. I have not yet found a way around this. Add Handler impl for up to 3 args Ah ha! I found the secret! So it's true that you can not just take in any argument for the first one. That's essentially special-cased. But, assuming that most functions' first argument would be the event itself (atspi::Event = Request), then we can make that the first argument, followed by N arguments that implement `From` (roughly). This will allow us to implement `From` for various types, which will be wrappers of more widely available types, for example: - Current/LastFocusedItem(CacheItem) - Current/LastCursorPos(CacheItem, u32) - EventItem(CacheItem) - etc. Add generic first parameter for Handler This allows the first parameter to be any type that implements `TryFrom`. It currently requires an unwrap due to the possibility of it being the wrong event type. This should be converted into its own Layer type, which can easily wrap the inner service, instead of manually implementing the conversion within the `Service` impl for `Handler<_>` types. * Implement generic first parameters for Handler Combined with the new atspi_event_handler function, automatically convert a HandlerService for a specific event type, into a generic event handler, which checks if the event type matches before passing it into the inner event handler. It will either: transform the event into its specific type, or abort the calling of the service with an `OdiliaError::AtspiError(AtspiError::Conversion(...))` * [WIP] attempt service-based event extractors * [WIP] Use layers to compose services instead of chaining services with combinators * You can now add boxed event handlers to the handler list * [WIP] remove Filter functions * [WIP] tower reader * [WIP] run all events handlers bound to a particular event * [WIP] first use of tower-based Odilia * [WIP] update SSIP, add speech to certain events * [WIP] return set of commands, instead of unit * [WIP] await all futures for an ev type in serial This is done to help with ordering caching handlers. It is important that caching is done first. * [WIP] Add commands enum * Make Handler trait generic over response type * Add command handler implementations * [WIP] finished and implemented serial futures, MultiService * [WIP] remove multiservice * [WIP] SerialHandlers use concrete type * [WIP] handle sequential futures better * [WIP] using boxes, get further ahead * Update interfaces in accordance with new atspi version * Use event introspection Some cleanup * Fix loop that never terminates; clone for now * Expand command handler impls to 3 params * Add new command introspection path * Add command_handlers function * Use discriminants instead of random strings * Add SendError type from tokio; you need the 'tokio' feature enabled to get the conversions * Change trait bounds to allow any Command: TryInto * Remove unused deps that are now in common * Use the tokio feature for common so that we can access extra error types. * Create one and exactly one handler function for the command speak * Pass commands from handlers to command handlers with error handling; it currently works! * Remove Speech var from doc_loaded func * Add IntoCommands trait, and implement it for a tuple up to three * Use IntoCommands trait to bound response types * Add IntoCommands impl for Results which contain other Into items * Use R instead of Response alias * Add From<&str> for OdiliaCommand * Add change_priority to command handlers * Add TryIntoCommands * Rework trait bounds for additional flexibility * Use IntoCommands recursively * Use 'impl TryIntoCommand' for return type of event processor :) * Add more specific timing to logs * Add microsecond logtime * Add Speech(SsipClientAsync) AsyncTryFrom impl for state - One, we set up a few new traits: - AsyncTryFrom/AsyncTryInto - FromState - Second, we use those traits instead of `From` to get arguments to handler functions. This allows us (in theory) to get cached items of various kinds. - Third, this removes some old test code. - Fourth, this adds new trait bounds using feature flags to make this work. - `try_trait_v2` for `FromResidual` - `impl_trait_in_assoc_type` (ITIAT), which is at least already has its stabalization PR opened * Remove failure case for AccessiblePrimitive::from_event * Remove use of ? in now infallible from_event and state * Update benchmarks to work on latest atspi * Fix structure of caching fns * Adjust to new cache fn style * Add caching, AsyncTryInto layer - Add new CacheLayer that injects Arc as a second parameter of the inner Service. - Add new AsyncTryInto layer to generically convert from one type to another, then call the inner service once that has been done. - Makes sure to follow advice to use `std::mem::replace` instead of just plain copying. - Drop some trait bounds when unneeded. - Wrap all end-user events in EventItem where E is a specific event type from `atspi` that implements `EventProperties`. - The complexity is overwhelming :(, hoping to crate this out, use less `impl Future` in trait (since it isn't stable, maybe just Box for now?) - Also uses unstable trait `FromResidual` * Use dbus if no cache available for relation * Macroify the complex trait definitions - impl_handler - impl_from_state Both are generic over a variable number of parameters and error types, assuming the errors implement `Into` and the types implement various underlying traits. * Use more flexible join! macro instead of join*() * Update tower sub-module and split into various files * Bring TryIntoLayer/Service into its own file * Further split Handler trait and impl from Handlers struct * [WIP] rethinking the handler trait * [WIP] add desired layer layout * Rework Handler function to be much more generic * More generic-aware AsyncTry, auto impl for TryFromState * Genericise from_state, clone all over; remove macro for now * Genericize state service * Use new, less strict traits: require more trait bounds on *_listener functions * Add TryFromState impls, Command type * Use new types in handlers, import new traits * Remove unbounded channels * Remove source of possible deadlock? * Remove .await where it is no longer needed * Add futures-concurrency crate * Remove cache layer * Remove CacheProvider trait * Remove FromState in favour of TryFromState * Add IterService and ServiceSet - IterService provides a way to stack two services together, where one take the iterated output of the other - ServiceSet provides a way to run sets of identically-typed services in series, much like SerialFutures did for Futures, but for Services. - Both take advantage of `type Future = impl Future<...>`, relying on a full Rust 2024 compiler which is not here yet. * Remove serial_fut module * DO NOT PRINT THE CACHE! * Instead of not printing the cache, just make the debug output not so long * Do not debug param of function that uses zbus::Connection * Modify ServiceSet in accordance with real-world usage * Remove unused function, use new ServiceSet for event handlers * User ServiceSet * Add ChoiceService * Add ServiceNotFound error type * fmt * Use more convenience functions * Update deps to latest zbus, seems to fix a deadlock issue * Add tokio-console capability, add script for conveneicne * Add tokio-console feature to main odilia binary * Cleanups and new inner services - Remove unused deps - Use if let ... on loop { ... } instead of while let Some(...) - Switch to using the ChoiceService model - Remove attempt at "generic handlers"; handled by ChoiceService * Move to mutli-threaded runtime; add channel indirection on zbus messages * Add comments explaining channel redirection * Create CacheEvent from state + E (event) * Add clone for choice service * public new function in IterService * Use command sub-service to layer atspi responses * Use futures-concurrency as workspace package, add rt-multi-thread feature to tokio Move futures-concurrency to workspace * Stop using channels to communicate commands Stop using channels to communicate commands * Add instrumenting on try_from_state * Add caret_moved handler (which does not work due to no focus being recorded) * Various improvements to common - Move `AccessiblePrimitive` to common from cache - Add `CaretPos` and `Focus` variants to `Command` * Remove AccessiblePrimitive from cache * Add tracing option to common, make atspi-proxies a dep * Update AccessiblePrimitive location * Add AccessibleHistory and CurrentCaretPos state structs * Add focus changed and caret pos updater handlers * Update cargo lock * Speak entire item upon focus * Add ability to unwrap Infallible values, then map them * Add new ServiceExt API for DX * Use new Odilia-specific ServiceExt * Remove return type generic on HandlerService * Add iter_into combinator * Add static version of Chooser trait * Use ChooserStatic trait, iter_into combinator * Use boxed_clone combinator * Fix docs * Add nightly to CI temporarily * Use dtolnay instead of actions-rs actions * Adjust clippy lints * Fix some other CI issues with old versions of actions-rs * Fix notify private documentation * Fix type in CI * Add clippy component for clippy job * Make sure llvm-tools are installed for the coverage components * Setup nightly MSRV * Fix yaml error * Add StateChanged newtype with predicate trait implementations * Add new error for predicate failure * Add the refinement crate for additional types, and derived-deref for convenience * Add state_changed crate to tower subcrate * Add TODO on error variant * Add focused/unfocused handlers using new StateChanged type * Update lock file * Use latest atspi branch in object handlers * Fix bench imports * Update dashmap and use inlining for about a 10% speed improvement; tested by running benchmarks * Add more type aliases for simplicity * Use enabled field as bool * Use bool as predicate for state changed instead of i32 * Rename predicates * Use deref on CacheEvent * Use specific Focused/Unfocused types * Move CacheEvent into its own file; move Choice impls into choice * Add CacheEvent types * Remove TryFromState for CacheEvent from state * Use main atspi * Only speak 'doc loaded' on focused applications * Add specific nightly toolchain (for now) --- .github/workflows/ci.yml | 39 +- Cargo.lock | 828 ++++++++++++++++++++++++------ Cargo.toml | 13 +- cache/Cargo.toml | 2 +- cache/benches/load_test.rs | 12 +- cache/src/lib.rs | 202 ++------ common/Cargo.toml | 13 +- common/src/cache.rs | 119 +++++ common/src/command.rs | 160 ++++++ common/src/errors.rs | 37 ++ common/src/lib.rs | 2 + odilia-notify/src/urgency.rs | 2 +- odilia/Cargo.toml | 16 +- odilia/src/commands.rs | 9 + odilia/src/events/cache.rs | 4 +- odilia/src/events/mod.rs | 59 +-- odilia/src/events/object.rs | 56 +- odilia/src/logging.rs | 15 +- odilia/src/main.rs | 164 +++++- odilia/src/state.rs | 154 +++++- odilia/src/tower/README.md | 45 ++ odilia/src/tower/async_try.rs | 110 ++++ odilia/src/tower/cache_event.rs | 132 +++++ odilia/src/tower/choice.rs | 126 +++++ odilia/src/tower/from_state.rs | 67 +++ odilia/src/tower/handler.rs | 85 +++ odilia/src/tower/handlers.rs | 144 ++++++ odilia/src/tower/iter_svc.rs | 66 +++ odilia/src/tower/mod.rs | 18 + odilia/src/tower/service_ext.rs | 52 ++ odilia/src/tower/service_set.rs | 56 ++ odilia/src/tower/state_changed.rs | 270 ++++++++++ odilia/src/tower/state_svc.rs | 49 ++ odilia/src/tower/sync_try.rs | 65 +++ odilia/src/tower/unwrap_svc.rs | 47 ++ rust-toolchain.toml | 2 + scripts/tokio_console_run.sh | 2 + tts/Cargo.toml | 3 +- 38 files changed, 2794 insertions(+), 451 deletions(-) create mode 100644 common/src/cache.rs create mode 100644 common/src/command.rs create mode 100644 odilia/src/commands.rs create mode 100644 odilia/src/tower/README.md create mode 100644 odilia/src/tower/async_try.rs create mode 100644 odilia/src/tower/cache_event.rs create mode 100644 odilia/src/tower/choice.rs create mode 100644 odilia/src/tower/from_state.rs create mode 100644 odilia/src/tower/handler.rs create mode 100644 odilia/src/tower/handlers.rs create mode 100644 odilia/src/tower/iter_svc.rs create mode 100644 odilia/src/tower/mod.rs create mode 100644 odilia/src/tower/service_ext.rs create mode 100644 odilia/src/tower/service_set.rs create mode 100644 odilia/src/tower/state_changed.rs create mode 100644 odilia/src/tower/state_svc.rs create mode 100644 odilia/src/tower/sync_try.rs create mode 100644 odilia/src/tower/unwrap_svc.rs create mode 100644 rust-toolchain.toml create mode 100755 scripts/tokio_console_run.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33b817fb..892fa419 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,9 +33,9 @@ jobs: ${{ runner.os }}-${{ matrix.target }}-build-${{ env.cache-name }}- - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly target: ${{ matrix.target }} - name: Install Dependencies @@ -68,16 +68,13 @@ jobs: ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}- - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly + components: clippy - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: clippy - # "-- -D warnings" will make the job fail if their are clippy warnings - args: --workspace --no-deps -- -D warnings -W clippy::print_stdout + run: cargo clippy --tests --workspace --no-deps -- -D warnings rustfmt: runs-on: ubuntu-latest @@ -86,17 +83,14 @@ jobs: uses: actions/checkout@v3 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: profile: minimal - toolchain: stable + toolchain: nightly components: rustfmt - name: Run formatter - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all --check + run: cargo fmt --all --check rustdoc: runs-on: ubuntu-latest @@ -119,14 +113,12 @@ jobs: ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}- - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly - name: Generate Documentation - uses: actions-rs/cargo@v1 - with: - command: doc + run: cargo doc --workspace --document-private-items - name: Deploy Documentation uses: peaceiris/actions-gh-pages@v3 @@ -159,7 +151,10 @@ jobs: restore-keys: | ${{ runner.os }}-${{ matrix.target }}-build-${{ env.cache-name }}- - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + components: llvm-tools - name: cargo install llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: cargo generate lockfile @@ -199,7 +194,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly - name: Install Cargo MSRV Verifier run: cargo install cargo-msrv --force - name: Check MSRV Compliance diff --git a/Cargo.lock b/Cargo.lock index bb657954..191047da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -82,9 +82,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -99,14 +99,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "async-broadcast" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ - "event-listener 5.3.0", - "event-listener-strategy 0.5.2", + "event-listener 5.3.1", + "event-listener-strategy", "futures-core", "pin-project-lite", ] @@ -129,16 +135,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener-strategy 0.5.2", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" dependencies = [ "async-task", "concurrent-queue", @@ -153,7 +159,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", ] @@ -166,8 +172,8 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", "once_cell", @@ -195,17 +201,17 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.0", + "polling 3.7.1", "rustix 0.38.34", "slab", "tracing", @@ -223,29 +229,29 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.3.1", + "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" +checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a" dependencies = [ "async-channel 2.3.1", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener 5.3.0", + "event-listener 5.3.1", "futures-lite 2.3.0", "rustix 0.38.34", "tracing", @@ -265,12 +271,12 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" dependencies = [ - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "atomic-waker", "cfg-if", "futures-core", @@ -364,8 +370,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "atspi-common", "atspi-connection", @@ -375,8 +380,7 @@ dependencies = [ [[package]] name = "atspi-common" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "enumflags2", "serde", @@ -391,8 +395,7 @@ dependencies = [ [[package]] name = "atspi-connection" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "atspi-common", "atspi-proxies", @@ -403,8 +406,7 @@ dependencies = [ [[package]] name = "atspi-proxies" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "atspi-common", "serde", @@ -429,11 +431,56 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -444,6 +491,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "1.3.2" @@ -456,6 +509,18 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block" version = "0.1.6" @@ -473,12 +538,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ "async-channel 2.3.1", - "async-lock 3.3.0", "async-task", "futures-io", "futures-lite 2.3.0", @@ -517,9 +581,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-if" @@ -583,9 +647,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -593,21 +657,21 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.0", + "clap_lex 0.7.1", "strsim", ] [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -626,9 +690,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" @@ -645,6 +709,44 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console-api" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257c22cd7e487dd4a13d413beabc512c5052f0bc048db0da6a84c3d8a6142fd" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c4cc54bae66f7d9188996404abdf7fdfa23034ef8e43478c8810828abad758" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -660,6 +762,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.4.0" @@ -698,6 +809,15 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -741,11 +861,12 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" dependencies = [ "cfg-if", + "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -761,6 +882,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derived-deref" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805ef2023ccd65425743a91ecd11fc020979a0b01921db3104fb606d18a7b43e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "digest" version = "0.10.7" @@ -824,11 +956,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "enumflags2" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ "enumflags2_derive", "serde", @@ -836,9 +980,9 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", @@ -869,43 +1013,22 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.3" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] -[[package]] -name = "event-listener" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - [[package]] name = "event-listener-strategy" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.3.0", + "event-listener 5.3.1", "pin-project-lite", ] @@ -948,6 +1071,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -963,6 +1108,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dcae03ee5afa5ea17b1aebc793806b8ddfc6dc500e0b8e8e1eb30b9dad22c0" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -973,6 +1129,21 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b14ac911e85d57c5ea6eef76d7b4d4a3177ecd15f4bea2e61927e9e3823e19f" +dependencies = [ + "bitvec", + "futures-buffered", + "futures-core", + "futures-lite 1.13.0", + "pin-project", + "slab", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.30" @@ -1097,9 +1268,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "gloo-timers" @@ -1113,6 +1284,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.1" @@ -1139,6 +1329,19 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "heck" version = "0.4.1" @@ -1172,6 +1375,82 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "indenter" version = "0.3.3" @@ -1356,11 +1635,17 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "6d0d8b92cd8358e8d229c11df9358decae64d137c5be540952c5ca7b25aea768" [[package]] name = "memoffset" @@ -1380,6 +1665,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -1426,6 +1723,16 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify-rust" version = "4.11.0" @@ -1514,9 +1821,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -1530,23 +1837,32 @@ dependencies = [ "atspi-connection", "atspi-proxies", "circular-queue", - "clap 4.5.4", + "clap 4.5.7", + "console-subscriber", + "derived-deref", "eyre", "figment", "futures", + "futures-concurrency", + "futures-lite 2.3.0", "lazy_static", "odilia-cache", "odilia-common", "odilia-input", "odilia-notify", "odilia-tts", + "pin-project", + "refinement", "serde_json", "serde_plain", + "ssip", "ssip-client-async", + "static_assertions", "tokio", "tokio-test", "tokio-util", "toml", + "tower", "tracing", "tracing-error", "tracing-journald", @@ -1586,12 +1902,18 @@ version = "0.3.0" dependencies = [ "atspi", "atspi-common", + "atspi-proxies", "bitflags 1.3.2", + "enum_dispatch", "figment", "serde", "serde_plain", "smartstring", + "ssip", + "strum 0.26.2", "thiserror", + "tokio", + "tracing", "xdg", "zbus", ] @@ -1632,6 +1954,7 @@ name = "odilia-tts" version = "0.1.4" dependencies = [ "eyre", + "ssip", "ssip-client-async", "tokio", "tokio-util", @@ -1680,9 +2003,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1724,6 +2047,32 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1738,9 +2087,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", "fastrand 2.1.0", @@ -1793,9 +2142,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" dependencies = [ "cfg-if", "concurrent-queue", @@ -1829,9 +2178,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -1849,6 +2198,38 @@ dependencies = [ "yansi", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -1877,6 +2258,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1947,16 +2334,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "refinement" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c1f648652916ca373423bde65c39208b194d83b0d25936b619b21b11958506" + [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1970,13 +2363,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -1987,9 +2380,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" @@ -2053,18 +2446,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -2188,30 +2581,29 @@ dependencies = [ ] [[package]] -name = "ssip-client-async" -version = "0.12.0" +name = "ssip" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20c8da60d51225982d675776f18b55c6bd41763b01c03f431190e0ad99c9dbf" +checksum = "5fc609dd23978fbb3684835bbf22d075c6ba36bcada9f0e6884a94b0842d2593" dependencies = [ - "async-std", - "dirs", - "log", - "ssip-common", - "strum", - "strum_macros", + "strum_macros 0.24.3", "thiserror", - "tokio", ] [[package]] -name = "ssip-common" -version = "0.1.0" +name = "ssip-client-async" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193e9edf18417738259bf590224732d68c1eac3a486dd9ce101b8f9a443e7e4" +checksum = "bffa78d7a908c0617c2a18c9716efc75c6ee93946b01e684deb45c0cbc59f5fe" dependencies = [ - "strum", - "strum_macros", + "async-std", + "dirs", + "log", + "ssip", + "strum 0.24.1", + "strum_macros 0.24.3", "thiserror", + "tokio", ] [[package]] @@ -2232,6 +2624,15 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -2245,6 +2646,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.66", +] + [[package]] name = "syn" version = "1.0.109" @@ -2267,6 +2681,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sysinfo" version = "0.26.9" @@ -2281,6 +2701,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tauri-winrt-notification" version = "0.2.1" @@ -2371,9 +2797,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2388,11 +2814,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -2440,14 +2876,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.13", + "toml_edit 0.22.14", ] [[package]] @@ -2472,23 +2908,83 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.13", ] +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2589,6 +3085,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -2623,9 +3125,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" @@ -2661,6 +3163,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2820,9 +3331,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets 0.52.5", ] @@ -2986,13 +3497,22 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xdg" version = "2.5.2" @@ -3001,12 +3521,12 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "xdg-home" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3017,22 +3537,22 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zbus" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c3977a7aafa97b12b9a35d21cdcff9b0d2289762b14683f45d66b1ba6c48f" +checksum = "23915fcb26e7a9a9dc05fd93a9870d336d6d032cd7e8cebf1c5c37666489fdd5" dependencies = [ "async-broadcast", "async-executor", "async-fs", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", - "event-listener 5.3.0", + "event-listener 5.3.1", "futures-core", "futures-sink", "futures-util", @@ -3080,9 +3600,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe9de53245dcf426b7be226a4217dd5e339080e5d46e64a02d6e5dcbf90fca1" +checksum = "02bcca0b586d2f8589da32347b4784ba424c4891ed86aa5b50d5e88f6b2c4f5d" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index cfedceae..71d50ffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,12 @@ pre-release-hook = ["cargo", "fmt"] dependent-version = "upgrade" [workspace.dependencies] -atspi = { version = "0.22.0", default-features = false, features = ["tokio"] } -atspi-proxies = { version = "0.6.0", default-features = false, features = ["tokio"] } -atspi-common = { version = "0.6.0", default-features = false, features = ["tokio"] } -atspi-connection = { version = "0.6.0", default-features = false, features = ["tokio"] } -odilia-common = { version = "0.3.0", path = "./common" } +atspi = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +atspi-proxies = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +atspi-common = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +atspi-connection = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +futures-concurrency = { version = "7.6.1" } +odilia-common = { version = "0.3.0", path = "./common", features = ["tokio"] } odilia-cache = { version = "0.3.0", path = "./cache" } eyre = "0.6.8" nix = "0.26.2" @@ -49,7 +50,7 @@ tracing-log = "^0.1.3" tracing-subscriber = { version = "0.3.16", default-features = false, features = ["env-filter", "parking_lot"] } tracing-error = "^0.2.0" tracing-tree = "^0.2.2" -zbus = { version = "4.2", features = ["tokio"] } +zbus = { version = "4.3", features = ["tokio"] } serde_plain = "1.0.1" xdg = "2.5.2" diff --git a/cache/Cargo.toml b/cache/Cargo.toml index 9c70f14f..705e3f25 100644 --- a/cache/Cargo.toml +++ b/cache/Cargo.toml @@ -16,7 +16,7 @@ atspi.workspace = true atspi-proxies.workspace = true atspi-common.workspace = true odilia-common.workspace = true -dashmap = "5.4.0" +dashmap = { version = "6.0.1", features = ["inline"] } serde = "1.0.147" tokio.workspace = true tracing.workspace = true diff --git a/cache/benches/load_test.rs b/cache/benches/load_test.rs index 41a0f966..6c4762e4 100644 --- a/cache/benches/load_test.rs +++ b/cache/benches/load_test.rs @@ -5,11 +5,13 @@ use std::{ }; use atspi_connection::AccessibilityConnection; -use atspi_proxies::accessible::Accessible; use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; -use odilia_cache::{AccessiblePrimitive, Cache, CacheItem}; +use odilia_cache::{Cache, CacheItem}; -use odilia_common::errors::{CacheError, OdiliaError}; +use odilia_common::{ + cache::AccessiblePrimitive, + errors::{CacheError, OdiliaError}, +}; use tokio::select; use tokio_test::block_on; @@ -59,7 +61,7 @@ async fn traverse_up(children: Vec) { for child in children { let mut item = child.clone(); loop { - item = match item.parent().await { + item = match item.parent() { Ok(item) => item, Err(OdiliaError::Cache(CacheError::NoItem)) => { // Missing item from cache; there's always exactly one. @@ -121,7 +123,7 @@ async fn reads_while_writing(cache: Cache, ids: Vec, items: fn cache_benchmark(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); - let a11y = block_on(AccessibilityConnection::open()).unwrap(); + let a11y = block_on(AccessibilityConnection::new()).unwrap(); let zbus_connection = a11y.connection(); let zbus_items: Vec = load_items!("./zbus_docs_cache_items.json"); diff --git a/cache/src/lib.rs b/cache/src/lib.rs index 7ec62a4d..f6f84d8a 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -18,26 +18,25 @@ pub use accessible_ext::AccessibleExt; use std::{ collections::HashMap, + fmt::Debug, + ops::Deref, sync::{Arc, RwLock, Weak}, }; use atspi_common::{ - object_ref::ObjectRef, ClipType, CoordType, EventProperties, Granularity, InterfaceSet, - RelationType, Role, StateSet, + ClipType, CoordType, EventProperties, Granularity, InterfaceSet, RelationType, Role, + StateSet, }; use atspi_proxies::{accessible::AccessibleProxy, text::TextProxy}; use dashmap::DashMap; use fxhash::FxBuildHasher; use odilia_common::{ - errors::{AccessiblePrimitiveConversionError, CacheError, OdiliaError}, + cache::AccessiblePrimitive, + errors::{CacheError, OdiliaError}, result::OdiliaResult, }; use serde::{Deserialize, Serialize}; -use zbus::{ - names::OwnedUniqueName, - zvariant::{ObjectPath, OwnedObjectPath}, - CacheProperties, ProxyBuilder, -}; +use zbus::CacheProperties; trait AllText { async fn get_all_text(&self) -> Result; @@ -53,121 +52,6 @@ type CacheKey = AccessiblePrimitive; type InnerCache = DashMap>, FxBuildHasher>; type ThreadSafeCache = Arc; -#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] -/// A struct which represents the bare minimum of an accessible for purposes of caching. -/// This makes some *possibly eronious* assumptions about what the sender is. -pub struct AccessiblePrimitive { - /// The accessible ID, which is an arbitrary string specified by the application. - /// It is guaranteed to be unique per application. - /// Examples: - /// * /org/a11y/atspi/accessible/1234 - /// * /org/a11y/atspi/accessible/null - /// * /org/a11y/atspi/accessible/root - /// * /org/Gnome/GTK/abab22-bbbb33-2bba2 - pub id: String, - /// Assuming that the sender is ":x.y", this stores the (x,y) portion of this sender. - /// Examples: - /// * :1.1 (the first window has opened) - /// * :2.5 (a second session exists, where at least 5 applications have been lauinched) - /// * :1.262 (many applications have been started on this bus) - pub sender: smartstring::alias::String, -} -impl AccessiblePrimitive { - /// Convert into an [`atspi_proxies::accessible::AccessibleProxy`]. Must be async because the creation of an async proxy requires async itself. - /// # Errors - /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. - #[tracing::instrument(skip_all, level = "trace", ret, err)] - pub async fn into_accessible<'a>( - self, - conn: &zbus::Connection, - ) -> zbus::Result> { - let id = self.id; - let sender = self.sender.clone(); - let path: ObjectPath<'a> = id.try_into()?; - ProxyBuilder::new(conn) - .path(path)? - .destination(sender.as_str().to_owned())? - .cache_properties(CacheProperties::No) - .build() - .await - } - /// Convert into an [`atspi_proxies::text::TextProxy`]. Must be async because the creation of an async proxy requires async itself. - /// # Errors - /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. - #[tracing::instrument(skip_all, level = "trace", ret, err)] - pub async fn into_text<'a>(self, conn: &zbus::Connection) -> zbus::Result> { - let id = self.id; - let sender = self.sender.clone(); - let path: ObjectPath<'a> = id.try_into()?; - ProxyBuilder::new(conn) - .path(path)? - .destination(sender.as_str().to_owned())? - .cache_properties(CacheProperties::No) - .build() - .await - } - /// Turns any `atspi::event` type into an `AccessiblePrimitive`, the basic type which is used for keys in the cache. - /// # Errors - /// The errors are self-explanitory variants of the [`odilia_common::errors::AccessiblePrimitiveConversionError`]. - #[tracing::instrument(skip_all, level = "trace", ret, err)] - pub fn from_event( - event: &T, - ) -> Result { - let sender = event.sender(); - let path = event.path(); - let id = path.to_string(); - Ok(Self { id, sender: sender.as_str().into() }) - } -} -impl From for AccessiblePrimitive { - fn from(atspi_accessible: ObjectRef) -> AccessiblePrimitive { - let tuple_converter = (atspi_accessible.name, atspi_accessible.path); - tuple_converter.into() - } -} - -impl From<(OwnedUniqueName, OwnedObjectPath)> for AccessiblePrimitive { - fn from(so: (OwnedUniqueName, OwnedObjectPath)) -> AccessiblePrimitive { - let accessible_id = so.1; - AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.as_str().into() } - } -} -impl From<(String, OwnedObjectPath)> for AccessiblePrimitive { - #[tracing::instrument(level = "trace", ret)] - fn from(so: (String, OwnedObjectPath)) -> AccessiblePrimitive { - let accessible_id = so.1; - AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.into() } - } -} -impl<'a> From<(String, ObjectPath<'a>)> for AccessiblePrimitive { - #[tracing::instrument(level = "trace", ret)] - fn from(so: (String, ObjectPath<'a>)) -> AccessiblePrimitive { - AccessiblePrimitive { id: so.1.to_string(), sender: so.0.into() } - } -} -impl<'a> TryFrom<&AccessibleProxy<'a>> for AccessiblePrimitive { - type Error = AccessiblePrimitiveConversionError; - - #[tracing::instrument(level = "trace", ret, err)] - fn try_from(accessible: &AccessibleProxy<'_>) -> Result { - let accessible = accessible.inner(); - let sender = accessible.destination().as_str().into(); - let id = accessible.path().as_str().into(); - Ok(AccessiblePrimitive { id, sender }) - } -} -impl<'a> TryFrom> for AccessiblePrimitive { - type Error = AccessiblePrimitiveConversionError; - - #[tracing::instrument(level = "trace", ret, err)] - fn try_from(accessible: AccessibleProxy<'_>) -> Result { - let accessible = accessible.inner(); - let sender = accessible.destination().as_str().into(); - let id = accessible.path().as_str().into(); - Ok(AccessiblePrimitive { id, sender }) - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] /// A struct representing an accessible. To get any information from the cache other than the stored information like role, interfaces, and states, you will need to instantiate an [`atspi_proxies::accessible::AccessibleProxy`] or other `*Proxy` type from atspi to query further info. pub struct CacheItem { @@ -223,10 +107,10 @@ impl CacheItem { #[tracing::instrument(level = "trace", skip_all, ret, err)] pub async fn from_atspi_event( event: &T, - cache: Weak, + cache: Arc, connection: &zbus::Connection, ) -> OdiliaResult { - let a11y_prim = AccessiblePrimitive::from_event(event)?; + let a11y_prim = AccessiblePrimitive::from_event(event); accessible_to_cache_item(&a11y_prim.into_accessible(connection).await?, cache).await } /// Convert an [`atspi::CacheItem`] into a [`crate::CacheItem`]. @@ -431,28 +315,17 @@ impl CacheItem { &self, ) -> Result)>, OdiliaError> { let cache = strong_cache(&self.cache)?; - as_accessible(self) - .await? - .get_relation_set() - .await? - .into_iter() - .map(|(relation, object_pairs)| { - ( - relation, - object_pairs - .into_iter() - .map(|object_pair| { - cache.get(&object_pair.into()).ok_or( - OdiliaError::Cache( - CacheError::NoItem, - ), - ) - }) - .collect::, OdiliaError>>(), - ) - }) - .map(|(relation, result_selfs)| Ok((relation, result_selfs?))) - .collect::)>, OdiliaError>>() + let ipc_rs = as_accessible(self).await?.get_relation_set().await?; + let mut relations = Vec::new(); + for (relation, object_pairs) in ipc_rs { + let mut cache_keys = Vec::new(); + for object_pair in object_pairs { + let cached = cache.get_ipc(&object_pair.into()).await?; + cache_keys.push(cached); + } + relations.push((relation, cache_keys)); + } + Ok(relations) } /// See [`atspi_proxies::accessible::AccessibleProxy::get_child_at_index`] /// # Errors @@ -722,19 +595,25 @@ impl CacheItem { /// This contains (mostly) all accessibles in the entire accessibility tree, and /// they are referenced by their IDs. If you are having issues with incorrect or /// invalid accessibles trying to be accessed, this is code is probably the issue. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Cache { pub by_id: ThreadSafeCache, pub connection: zbus::Connection, } +impl std::fmt::Debug for Cache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("Cache {{ by_id: ...{} items..., .. }}", self.by_id.len())) + } +} + // N.B.: we are using std RwLockes internally here, within the cache hashmap // entries. When adding async methods, take care not to hold these mutexes // across .await points. impl Cache { /// create a new, fresh cache #[must_use] - #[tracing::instrument(level = "debug", ret)] + #[tracing::instrument(level = "debug", ret, skip_all)] pub fn new(conn: zbus::Connection) -> Self { Self { by_id: Arc::new(DashMap::with_capacity_and_hasher( @@ -793,6 +672,18 @@ impl Cache { Some(self.by_id.get(id).as_deref()?.read().ok()?.clone()) } + /// Get a single item from the cache. This will also get the information from DBus if it does not + /// exist in the cache. + #[must_use] + #[tracing::instrument(level = "trace", ret)] + pub async fn get_ipc(&self, id: &CacheKey) -> Result { + if let Some(ci) = self.get(id) { + return Ok(ci); + } + let acc = id.clone().into_accessible(&self.connection).await?; + accessible_to_cache_item(&acc, self).await + } + /// get a many items from the cache; this only creates one read handle (note that this will copy all data you would like to access) #[must_use] #[tracing::instrument(level = "trace", ret)] @@ -865,7 +756,7 @@ impl Cache { pub async fn get_or_create( &self, accessible: &AccessibleProxy<'_>, - cache: Weak, + cache: Arc, ) -> OdiliaResult { // if the item already exists in the cache, return it let primitive = accessible.try_into()?; @@ -925,6 +816,11 @@ impl Cache { } Ok(()) } + pub async fn from_event(&self, ev: &T) -> OdiliaResult { + let a11y_prim = AccessiblePrimitive::from_event(ev); + accessible_to_cache_item(&a11y_prim.into_accessible(&self.connection).await?, self) + .await + } } /// Convert an [`atspi_proxies::accessible::AccessibleProxy`] into a [`crate::CacheItem`]. @@ -939,9 +835,9 @@ impl Cache { /// 2. Any of the function calls on the `accessible` fail. /// 3. Any `(String, OwnedObjectPath) -> AccessiblePrimitive` conversions fail. This *should* never happen, but technically it is possible. #[tracing::instrument(level = "trace", ret, err)] -pub async fn accessible_to_cache_item( +pub async fn accessible_to_cache_item + Debug>( accessible: &AccessibleProxy<'_>, - cache: Weak, + cache: C, ) -> OdiliaResult { let (app, parent, index, children_num, interfaces, role, states, children) = tokio::try_join!( accessible.get_application(), @@ -971,6 +867,6 @@ pub async fn accessible_to_cache_item( states, text, children: children.into_iter().map(|k| CacheRef::new(k.into())).collect(), - cache, + cache: Arc::downgrade(&Arc::new(cache.deref().clone())), }) } diff --git a/common/Cargo.toml b/common/Cargo.toml index f234421f..6cc82321 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -11,9 +11,15 @@ keywords = ["screen-reader", "accessibility", "a11y", "data-structures", "linux" categories = ["accessibility"] edition = "2021" +[features] +default = [] +tokio = ["dep:tokio"] +tracing = ["dep:tracing"] + [dependencies] atspi.workspace = true atspi-common.workspace = true +atspi-proxies.workspace = true bitflags = "1.3.2" serde = "1.0.147" smartstring = "1.0.1" @@ -21,4 +27,9 @@ thiserror = "1.0.37" zbus.workspace = true serde_plain.workspace = true figment = "0.10.15" -xdg.workspace=true +enum_dispatch = "0.3.13" +strum = { version = "0.26.2", features = ["derive"] } +tokio = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } +ssip = "0.1.0" +xdg.workspace = true diff --git a/common/src/cache.rs b/common/src/cache.rs new file mode 100644 index 00000000..2f7f3d85 --- /dev/null +++ b/common/src/cache.rs @@ -0,0 +1,119 @@ +use crate::{errors::AccessiblePrimitiveConversionError, ObjectPath}; +use atspi::{EventProperties, ObjectRef}; +use atspi_proxies::{accessible::AccessibleProxy, text::TextProxy}; +use serde::{Deserialize, Serialize}; +use zbus::{names::OwnedUniqueName, zvariant::OwnedObjectPath, CacheProperties, ProxyBuilder}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +/// A struct which represents the bare minimum of an accessible for purposes of caching. +/// This makes some *possibly eronious* assumptions about what the sender is. +pub struct AccessiblePrimitive { + /// The accessible ID, which is an arbitrary string specified by the application. + /// It is guaranteed to be unique per application. + /// Examples: + /// * /org/a11y/atspi/accessible/1234 + /// * /org/a11y/atspi/accessible/null + /// * /org/a11y/atspi/accessible/root + /// * /org/Gnome/GTK/abab22-bbbb33-2bba2 + pub id: String, + /// Assuming that the sender is ":x.y", this stores the (x,y) portion of this sender. + /// Examples: + /// * :1.1 (the first window has opened) + /// * :2.5 (a second session exists, where at least 5 applications have been lauinched) + /// * :1.262 (many applications have been started on this bus) + pub sender: smartstring::alias::String, +} + +impl AccessiblePrimitive { + /// Turns any `atspi::event` type into an `AccessiblePrimitive`, the basic type which is used for keys in the cache. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + pub fn from_event(event: &T) -> Self { + let sender = event.sender(); + let path = event.path(); + let id = path.to_string(); + Self { id, sender: sender.as_str().into() } + } + + /// Convert into an [`atspi_proxies::accessible::AccessibleProxy`]. Must be async because the creation of an async proxy requires async itself. + /// # Errors + /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + pub async fn into_accessible<'a>( + self, + conn: &zbus::Connection, + ) -> zbus::Result> { + let id = self.id; + let sender = self.sender.clone(); + let path: ObjectPath<'a> = id.try_into()?; + ProxyBuilder::new(conn) + .path(path)? + .destination(sender.as_str().to_owned())? + .cache_properties(CacheProperties::No) + .build() + .await + } + /// Convert into an [`atspi_proxies::text::TextProxy`]. Must be async because the creation of an async proxy requires async itself. + /// # Errors + /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + pub async fn into_text<'a>(self, conn: &zbus::Connection) -> zbus::Result> { + let id = self.id; + let sender = self.sender.clone(); + let path: ObjectPath<'a> = id.try_into()?; + ProxyBuilder::new(conn) + .path(path)? + .destination(sender.as_str().to_owned())? + .cache_properties(CacheProperties::No) + .build() + .await + } +} + +impl From for AccessiblePrimitive { + fn from(atspi_accessible: ObjectRef) -> AccessiblePrimitive { + let tuple_converter = (atspi_accessible.name, atspi_accessible.path); + tuple_converter.into() + } +} + +impl From<(OwnedUniqueName, OwnedObjectPath)> for AccessiblePrimitive { + fn from(so: (OwnedUniqueName, OwnedObjectPath)) -> AccessiblePrimitive { + let accessible_id = so.1; + AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.as_str().into() } + } +} +impl From<(String, OwnedObjectPath)> for AccessiblePrimitive { + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret))] + fn from(so: (String, OwnedObjectPath)) -> AccessiblePrimitive { + let accessible_id = so.1; + AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.into() } + } +} +impl<'a> From<(String, ObjectPath<'a>)> for AccessiblePrimitive { + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret))] + fn from(so: (String, ObjectPath<'a>)) -> AccessiblePrimitive { + AccessiblePrimitive { id: so.1.to_string(), sender: so.0.into() } + } +} +impl<'a> TryFrom<&AccessibleProxy<'a>> for AccessiblePrimitive { + type Error = AccessiblePrimitiveConversionError; + + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + fn try_from(accessible: &AccessibleProxy<'_>) -> Result { + let accessible = accessible.inner(); + let sender = accessible.destination().as_str().into(); + let id = accessible.path().as_str().into(); + Ok(AccessiblePrimitive { id, sender }) + } +} +impl<'a> TryFrom> for AccessiblePrimitive { + type Error = AccessiblePrimitiveConversionError; + + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + fn try_from(accessible: AccessibleProxy<'_>) -> Result { + let accessible = accessible.inner(); + let sender = accessible.destination().as_str().into(); + let id = accessible.path().as_str().into(); + Ok(AccessiblePrimitive { id, sender }) + } +} diff --git a/common/src/command.rs b/common/src/command.rs new file mode 100644 index 00000000..8b8ece51 --- /dev/null +++ b/common/src/command.rs @@ -0,0 +1,160 @@ +#![allow(clippy::module_name_repetitions)] + +use crate::cache::AccessiblePrimitive; +use crate::errors::OdiliaError; +use enum_dispatch::enum_dispatch; +use ssip::Priority; +use std::convert::Infallible; + +use strum::{Display, EnumDiscriminants}; + +pub trait TryIntoCommands { + type Error: Into; + /// Fallibly returns a [`Vec`] of [`OdiliaCommand`]s to run. + /// + /// # Errors + /// + /// When implemented, the function is allowed to fail with any type that can be converted into + /// [`OdiliaError`], but conversion should between these types should be done from the + /// implementers' side, liekly using `?`. + fn try_into_commands(self) -> Result, OdiliaError>; +} +impl TryIntoCommands for Result, OdiliaError> { + type Error = OdiliaError; + fn try_into_commands(self) -> Result, OdiliaError> { + self + } +} +impl TryIntoCommands for T { + type Error = Infallible; + fn try_into_commands(self) -> Result, OdiliaError> { + Ok(self.into_commands()) + } +} +impl> TryIntoCommands for Result { + type Error = E; + fn try_into_commands(self) -> Result, OdiliaError> { + match self { + Ok(ok) => Ok(ok.into_commands()), + Err(err) => Err(err.into()), + } + } +} + +pub trait IntoCommands { + fn into_commands(self) -> Vec; +} + +impl IntoCommands for CaretPos { + fn into_commands(self) -> Vec { + vec![self.into()] + } +} +impl IntoCommands for Focus { + fn into_commands(self) -> Vec { + vec![self.into()] + } +} +impl IntoCommands for (Priority, &str) { + fn into_commands(self) -> Vec { + vec![Speak(self.1.to_string(), self.0).into()] + } +} +impl IntoCommands for (Priority, String) { + fn into_commands(self) -> Vec { + vec![Speak(self.1, self.0).into()] + } +} +impl IntoCommands for () { + fn into_commands(self) -> Vec { + vec![] + } +} +impl IntoCommands for (T1,) +where + T1: IntoCommands, +{ + fn into_commands(self) -> Vec { + self.0.into_commands() + } +} +impl IntoCommands for (T1, T2) +where + T1: IntoCommands, + T2: IntoCommands, +{ + fn into_commands(self) -> Vec { + let mut ret = self.0.into_commands(); + ret.extend(self.1.into_commands()); + ret + } +} +impl IntoCommands for (T1, T2, T3) +where + T1: IntoCommands, + T2: IntoCommands, + T3: IntoCommands, +{ + fn into_commands(self) -> Vec { + let mut ret = self.0.into_commands(); + ret.extend(self.1.into_commands()); + ret.extend(self.2.into_commands()); + ret + } +} +impl IntoCommands for (T1, T2, T3, T4) +where + T1: IntoCommands, + T2: IntoCommands, + T3: IntoCommands, + T4: IntoCommands, +{ + fn into_commands(self) -> Vec { + let mut ret = self.0.into_commands(); + ret.extend(self.1.into_commands()); + ret.extend(self.2.into_commands()); + ret.extend(self.3.into_commands()); + ret + } +} + +pub trait CommandType { + const CTYPE: OdiliaCommandDiscriminants; +} +#[enum_dispatch] +pub trait CommandTypeDynamic { + fn ctype(&self) -> OdiliaCommandDiscriminants; +} +impl CommandTypeDynamic for T { + fn ctype(&self) -> OdiliaCommandDiscriminants { + T::CTYPE + } +} + +#[derive(Debug, Clone)] +pub struct CaretPos(pub usize); + +#[derive(Debug, Clone)] +pub struct Speak(pub String, pub Priority); + +#[derive(Debug, Clone)] +pub struct Focus(pub AccessiblePrimitive); + +impl CommandType for Speak { + const CTYPE: OdiliaCommandDiscriminants = OdiliaCommandDiscriminants::Speak; +} +impl CommandType for Focus { + const CTYPE: OdiliaCommandDiscriminants = OdiliaCommandDiscriminants::Focus; +} +impl CommandType for CaretPos { + const CTYPE: OdiliaCommandDiscriminants = OdiliaCommandDiscriminants::CaretPos; +} + +#[derive(Debug, Clone, EnumDiscriminants)] +#[strum_discriminants(derive(Ord, PartialOrd, Display))] +#[enum_dispatch(CommandTypeDynamic)] +pub enum OdiliaCommand { + Speak(Speak), + Focus(Focus), + CaretPos(CaretPos), +} diff --git a/common/src/errors.rs b/common/src/errors.rs index 271ecbba..8a7dd4fe 100644 --- a/common/src/errors.rs +++ b/common/src/errors.rs @@ -1,3 +1,4 @@ +use crate::command::OdiliaCommand; use atspi::AtspiError; use atspi_common::AtspiError as AtspiTypesError; use serde_plain::Error as SerdePlainError; @@ -14,13 +15,49 @@ pub enum OdiliaError { Zbus(zbus::Error), ZbusFdo(zbus::fdo::Error), Zvariant(zbus::zvariant::Error), + SendError(SendError), Cache(CacheError), InfallibleConversion(std::convert::Infallible), ConversionError(std::num::TryFromIntError), Config(ConfigError), PoisoningError, Generic(String), + Static(&'static str), + ServiceNotFound(String), + PredicateFailure(String), } + +impl From<&'static str> for OdiliaError { + fn from(s: &'static str) -> OdiliaError { + Self::Static(s) + } +} + +#[derive(Debug)] +pub enum SendError { + Atspi(atspi::Event), + Command(OdiliaCommand), + Ssip(ssip::Request), +} + +macro_rules! send_err_impl { + ($tokio_err:ty, $variant:path) => { + #[cfg(feature = "tokio")] + impl From<$tokio_err> for OdiliaError { + fn from(t_err: $tokio_err) -> OdiliaError { + OdiliaError::SendError($variant(t_err.0)) + } + } + }; +} + +send_err_impl!(tokio::sync::broadcast::error::SendError, SendError::Atspi); +send_err_impl!(tokio::sync::mpsc::error::SendError, SendError::Atspi); +send_err_impl!(tokio::sync::broadcast::error::SendError, SendError::Command); +send_err_impl!(tokio::sync::mpsc::error::SendError, SendError::Command); +send_err_impl!(tokio::sync::broadcast::error::SendError, SendError::Ssip); +send_err_impl!(tokio::sync::mpsc::error::SendError, SendError::Ssip); + #[derive(Debug)] pub enum ConfigError { Figment(figment::Error), diff --git a/common/src/lib.rs b/common/src/lib.rs index 61037eff..c54f3cf6 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -9,6 +9,8 @@ use zbus::{names::UniqueName, zvariant::ObjectPath}; +pub mod cache; +pub mod command; pub mod elements; pub mod errors; pub mod events; diff --git a/odilia-notify/src/urgency.rs b/odilia-notify/src/urgency.rs index 7924a37e..be16d2db 100644 --- a/odilia-notify/src/urgency.rs +++ b/odilia-notify/src/urgency.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use zbus::zvariant::{OwnedValue, Type, Value}; /// A priority/urgency level. -/// https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#urgency-levels +/// [See specification here](https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#urgency-levels) #[derive( Clone, Copy, diff --git a/odilia/Cargo.toml b/odilia/Cargo.toml index 0e30265a..b75c0e03 100644 --- a/odilia/Cargo.toml +++ b/odilia/Cargo.toml @@ -14,7 +14,7 @@ homepage = "https://odilia.app" keywords = ["screen-reader", "accessibility", "a11y", "tts", "linux"] categories = ["accessibility"] edition = "2021" -rust-version = "1.75" +rust-version = "1.81" publish = true [package.metadata.release] @@ -43,8 +43,8 @@ odilia-input = { path = "../input", version = "0.0.3" } odilia-tts = { path = "../tts", version = "0.1.4" } serde_json.workspace = true serde_plain.workspace = true -ssip-client-async.workspace = true -tokio.workspace = true +ssip-client-async = { version = "0.13.0", features = ["tokio"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } tracing-error.workspace = true tracing-log.workspace = true tracing-subscriber.workspace = true @@ -58,9 +58,19 @@ tokio-util.workspace=true toml = "0.8.11" figment = { version = "0.10.14", features = ["env", "toml"] } tracing-journald = "0.3.0" +tower = { version = "0.4.13", features = ["util", "filter", "steer"] } +ssip = "0.1.0" +futures-lite = "2.3.0" +pin-project = "1.1.5" +static_assertions = "1.1.0" +futures-concurrency.workspace = true +console-subscriber = { version = "0.3.0", optional = true } +refinement = "0.5.0" +derived-deref = "2.1.0" [dev-dependencies] lazy_static = "1.4.0" tokio-test = "0.4.2" [features] +tokio-console = ["dep:console-subscriber"] diff --git a/odilia/src/commands.rs b/odilia/src/commands.rs new file mode 100644 index 00000000..d1e454c3 --- /dev/null +++ b/odilia/src/commands.rs @@ -0,0 +1,9 @@ +use crate::tower::Handler; +use odilia_common::command::OdiliaCommand as Command; +use odilia_common::errors::OdiliaError; +use std::future::Future; + +type Request = Command; +type Response = (); +type Error = OdiliaError; + diff --git a/odilia/src/events/cache.rs b/odilia/src/events/cache.rs index e8fe0690..5dc14319 100644 --- a/odilia/src/events/cache.rs +++ b/odilia/src/events/cache.rs @@ -2,7 +2,7 @@ use crate::ScreenReaderState; use atspi::events::{ AddAccessibleEvent, CacheEvents, LegacyAddAccessibleEvent, RemoveAccessibleEvent, }; -use odilia_cache::AccessiblePrimitive; +use odilia_common::cache::AccessiblePrimitive; #[tracing::instrument(level = "debug", skip(state), ret, err)] pub async fn dispatch(state: &ScreenReaderState, event: &CacheEvents) -> eyre::Result<()> { @@ -41,7 +41,7 @@ pub fn remove_accessible( state: &ScreenReaderState, event: &RemoveAccessibleEvent, ) -> eyre::Result<()> { - let accessible_prim: AccessiblePrimitive = AccessiblePrimitive::from_event(event)?; + let accessible_prim: AccessiblePrimitive = AccessiblePrimitive::from_event(event); state.cache.remove(&accessible_prim); Ok(()) } diff --git a/odilia/src/events/mod.rs b/odilia/src/events/mod.rs index e2ded987..0a8079ac 100644 --- a/odilia/src/events/mod.rs +++ b/odilia/src/events/mod.rs @@ -26,7 +26,7 @@ pub async fn structural_navigation( role: Role, ) -> OdiliaResult { tracing::debug!("Structural nav call begins!"); - let curr = match state.history_item(0).await { + let curr = match state.history_item(0) { Some(acc) => acc.into_accessible(state.atspi.connection()).await?, None => return Ok(false), }; @@ -36,7 +36,7 @@ pub async fn structural_navigation( let curr_prim = curr.try_into()?; let _: bool = comp.grab_focus().await?; comp.scroll_to(ScrollType::TopLeft).await?; - state.update_accessible(curr_prim).await; + state.update_accessible(curr_prim); let _: bool = texti.set_caret_offset(0).await?; let role = next.get_role().await?; let len = texti.character_count().await?; @@ -58,34 +58,35 @@ pub async fn sr_event( ) -> eyre::Result<()> { loop { tokio::select! { - sr_event = sr_events.recv() => { - tracing::debug!("SR Event received"); - match sr_event { - Some(ScreenReaderEvent::StructuralNavigation(dir, role)) => { - if let Err(e) = structural_navigation(&state, dir, role).await { - tracing::debug!(error = %e, "There was an error with the structural navigation call."); - } else { - tracing::debug!("Structural navigation successful!"); + sr_event = sr_events.recv() => { + tracing::debug!("SR Event received"); + match sr_event { + Some(ScreenReaderEvent::StructuralNavigation(dir, role)) => { + if let Err(e) = structural_navigation(&state, dir, role).await { + tracing::debug!(error = %e, "There was an error with the structural navigation call."); + } else { + tracing::debug!("Structural navigation successful!"); + } + }, + Some(ScreenReaderEvent::StopSpeech) => { + tracing::debug!("Stopping speech!"); + state.stop_speech().await; + }, + Some(ScreenReaderEvent::ChangeMode(new_sr_mode)) => { + tracing::debug!("Changing mode to {:?}", new_sr_mode); + if let Ok(mut sr_mode) = state.mode.lock() { + *sr_mode = new_sr_mode; + } } - }, - Some(ScreenReaderEvent::StopSpeech) => { - tracing::debug!("Stopping speech!"); - state.stop_speech().await; - }, - Some(ScreenReaderEvent::ChangeMode(new_sr_mode)) => { - tracing::debug!("Changing mode to {:?}", new_sr_mode); - let mut sr_mode = state.mode.lock().await; - *sr_mode = new_sr_mode; - } - _ => { continue; } - }; - continue; - } - () = shutdown.cancelled() => { - tracing::debug!("sr_event cancelled"); - break; + _ => { continue; } + }; + continue; + } + () = shutdown.cancelled() => { + tracing::debug!("sr_event cancelled"); + break; + } } - } } Ok(()) } @@ -175,7 +176,7 @@ async fn dispatch(state: &ScreenReaderState, event: Event) -> eyre::Result<()> { ); } } - state.event_history_update(event).await; + state.event_history_update(event); Ok(()) } diff --git a/odilia/src/events/object.rs b/odilia/src/events/object.rs index 61b28b40..c84ceac1 100644 --- a/odilia/src/events/object.rs +++ b/odilia/src/events/object.rs @@ -26,7 +26,7 @@ pub async fn dispatch(state: &ScreenReaderState, event: &ObjectEvents) -> eyre:: mod text_changed { use crate::state::ScreenReaderState; - use atspi_common::events::object::TextChangedEvent; + use atspi_common::{events::object::TextChangedEvent, Operation}; use odilia_cache::CacheItem; use odilia_common::{ errors::OdiliaError, @@ -160,15 +160,9 @@ mod text_changed { state: &ScreenReaderState, event: &TextChangedEvent, ) -> eyre::Result<()> { - match event.operation.as_str() { - "insert/system" => insert_or_delete(state, event, true).await?, - "insert" => insert_or_delete(state, event, true).await?, - "delete/system" => insert_or_delete(state, event, false).await?, - "delete" => insert_or_delete(state, event, false).await?, - _ => tracing::trace!( - "TextChangedEvent has invalid kind: {}", - event.operation - ), + match event.operation { + Operation::Insert => insert_or_delete(state, event, true).await?, + Operation::Delete => insert_or_delete(state, event, false).await?, }; Ok(()) } @@ -246,9 +240,9 @@ mod text_changed { mod children_changed { use crate::state::ScreenReaderState; - use atspi_common::events::object::ChildrenChangedEvent; - use odilia_cache::{AccessiblePrimitive, CacheItem}; - use odilia_common::result::OdiliaResult; + use atspi_common::{events::object::ChildrenChangedEvent, Operation}; + use odilia_cache::CacheItem; + use odilia_common::{cache::AccessiblePrimitive, result::OdiliaResult}; use std::sync::Arc; #[tracing::instrument(level = "debug", skip(state), err)] @@ -257,10 +251,9 @@ mod children_changed { event: &ChildrenChangedEvent, ) -> eyre::Result<()> { // Dispatch based on kind - match event.operation.as_str() { - "remove" | "remove/system" => remove(state, event)?, - "add" | "add/system" => add(state, event).await?, - kind => tracing::debug!(kind, "Ignoring event with unknown kind"), + match event.operation { + Operation::Insert => add(state, event).await?, + Operation::Delete => remove(state, event)?, } Ok(()) } @@ -272,10 +265,8 @@ mod children_changed { let accessible = get_child_primitive(event) .into_accessible(state.atspi.connection()) .await?; - let _: OdiliaResult = state - .cache - .get_or_create(&accessible, Arc::downgrade(&Arc::clone(&state.cache))) - .await; + let _: OdiliaResult = + state.cache.get_or_create(&accessible, Arc::clone(&state.cache)).await; tracing::debug!("Add a single item to cache."); Ok(()) } @@ -369,7 +360,7 @@ mod text_caret_moved { return Ok(false); } // Hopefully this shouldn't happen, but technically the caret may change before any other event happens. Since we already know that the caret position is 0, it may be a caret moved event - let last_accessible = match state.history_item(0).await { + let last_accessible = match state.history_item(0) { Some(acc) => state.get_or_create_cache_item(acc).await?, None => return Ok(true), }; @@ -398,7 +389,7 @@ mod text_caret_moved { let new_item = state.get_or_create_event_object_to_cache(event).await?; let new_prim = new_item.object.clone(); - let text = match state.history_item(0).await { + let text = match state.history_item(0) { Some(old_prim) => { let old_pos = state.previous_caret_position.load(Ordering::Relaxed); let old_item = @@ -419,7 +410,7 @@ mod text_caret_moved { } }; state.say(Priority::Text, text).await; - state.update_accessible(new_prim).await; + state.update_accessible(new_prim); Ok(()) } @@ -443,7 +434,7 @@ mod text_caret_moved { mod state_changed { use crate::state::ScreenReaderState; use atspi_common::{events::object::StateChangedEvent, State}; - use odilia_cache::AccessiblePrimitive; + use odilia_common::cache::AccessiblePrimitive; /// Update the state of an item in the cache using a `StateChanged` event and the `ScreenReaderState` as context. /// This writes to the value in-place, and does not clone any values. @@ -469,16 +460,16 @@ mod state_changed { state: &ScreenReaderState, event: &StateChangedEvent, ) -> eyre::Result<()> { - let state_value = event.enabled == 1; + let state_value = event.enabled; // update cache with state of item - let a11y_prim = AccessiblePrimitive::from_event(event)?; + let a11y_prim = AccessiblePrimitive::from_event(event); if update_state(state, &a11y_prim, event.state, state_value)? { tracing::trace!("Updating of the state was not successful! The item with id {:?} was not found in the cache.", a11y_prim.id); } else { tracing::trace!("Updated the state of accessible with ID {:?}, and state {:?} to {state_value}.", a11y_prim.id, event.state); } // enabled can only be 1 or 0, but is not a boolean over dbus - match (event.state, event.enabled == 1) { + match (event.state, event.enabled) { (State::Focused, true) => focused(state, event).await?, (state, enabled) => tracing::trace!( "Ignoring state_changed event with unknown kind: {:?}/{}", @@ -495,7 +486,7 @@ mod state_changed { event: &StateChangedEvent, ) -> eyre::Result<()> { let accessible = state.get_or_create_event_object_to_cache(event).await?; - if let Some(curr) = state.history_item(0).await { + if let Some(curr) = state.history_item(0) { if curr == accessible.object { return Ok(()); } @@ -506,7 +497,7 @@ mod state_changed { accessible.description(), accessible.get_relation_set(), )?; - state.update_accessible(accessible.object.clone()).await; + state.update_accessible(accessible.object.clone()); tracing::debug!( "Focus event received on: {:?} with role {}", accessible.object.id, @@ -520,7 +511,7 @@ mod state_changed { ) .await; - state.update_accessible(accessible.object).await; + state.update_accessible(accessible.object); Ok(()) } } @@ -531,7 +522,8 @@ mod tests { use atspi_common::{Interface, InterfaceSet, Role, State, StateSet}; use atspi_connection::AccessibilityConnection; use lazy_static::lazy_static; - use odilia_cache::{AccessiblePrimitive, Cache, CacheItem}; + use odilia_cache::{Cache, CacheItem}; + use odilia_common::cache::AccessiblePrimitive; use std::sync::Arc; use tokio_test::block_on; diff --git a/odilia/src/logging.rs b/odilia/src/logging.rs index c79e53c2..76a06305 100644 --- a/odilia/src/logging.rs +++ b/odilia/src/logging.rs @@ -9,6 +9,7 @@ use eyre::Context; use odilia_common::settings::{log::LoggingKind, ApplicationConfig}; use tracing_error::ErrorLayer; use tracing_subscriber::{prelude::*, EnvFilter}; +use tracing_tree::time::Uptime; use tracing_tree::HierarchicalLayer; /// Initialise the logging stack @@ -25,7 +26,8 @@ pub fn init(config: &ApplicationConfig) -> eyre::Result<()> { .with_span_retrace(true) .with_indent_lines(true) .with_ansi(false) - .with_wraparound(4); + .with_wraparound(4) + .with_timer(Uptime::default()); //this requires boxing because the types returned by this match block would be incompatible otherwise, since we return different layers, or modifications to a layer depending on what we get from the configuration. It is possible to do it otherwise, hopefully, but for now this would do let final_layer = match &config.log.logger { LoggingKind::File(path) => { @@ -39,7 +41,16 @@ pub fn init(config: &ApplicationConfig) -> eyre::Result<()> { .with_syslog_identifier("odilia".to_owned()) .boxed(), }; - tracing_subscriber::Registry::default() + #[cfg(feature = "tokio-console")] + let trace_sub = { + let console_layer = console_subscriber::spawn(); + tracing_subscriber::Registry::default() + .with(EnvFilter::from("tokio=trace,runtime=trace")) + .with(console_layer) + }; + #[cfg(not(feature = "tokio-console"))] + let trace_sub = { tracing_subscriber::Registry::default() }; + trace_sub .with(env_filter) .with(ErrorLayer::default()) .with(final_layer) diff --git a/odilia/src/main.rs b/odilia/src/main.rs index 632675fa..03ea3836 100644 --- a/odilia/src/main.rs +++ b/odilia/src/main.rs @@ -7,16 +7,26 @@ unsafe_code )] #![allow(clippy::multiple_crate_versions)] +#![feature(impl_trait_in_assoc_type)] mod cli; mod events; mod logging; mod state; +mod tower; use std::{fs, path::PathBuf, process::exit, sync::Arc, time::Duration}; use crate::cli::Args; +use crate::state::AccessibleHistory; +use crate::state::Command; +use crate::state::CurrentCaretPos; +use crate::state::LastCaretPos; +use crate::state::LastFocused; use crate::state::ScreenReaderState; +use crate::state::Speech; +use crate::tower::{CacheEvent, cache_event::ActiveAppEvent}; +use crate::tower::Handlers; use clap::Parser; use eyre::WrapErr; use figment::{ @@ -24,10 +34,15 @@ use figment::{ Figment, }; use futures::{future::FutureExt, StreamExt}; -use odilia_common::settings::ApplicationConfig; +use odilia_common::{ + command::{CaretPos, Focus, IntoCommands, OdiliaCommand, Speak, TryIntoCommands}, + errors::OdiliaError, + settings::ApplicationConfig, +}; use odilia_input::sr_event_receiver; use odilia_notify::listen_to_dbus_notifications; -use ssip_client_async::Priority; +use ssip::Priority; +use ssip::Request as SSIPRequest; use tokio::{ signal::unix::{signal, SignalKind}, sync::mpsc, @@ -78,8 +93,97 @@ async fn sigterm_signal_watcher( Ok(()) } -#[tracing::instrument] -#[tokio::main(flavor = "current_thread")] +use atspi::events::document::LoadCompleteEvent; +use atspi::events::object::TextCaretMovedEvent; +use atspi::Granularity; +use std::cmp::{max, min}; + +#[tracing::instrument(ret, err)] +async fn speak( + Command(Speak(text, priority)): Command, + Speech(ssip): Speech, +) -> Result<(), odilia_common::errors::OdiliaError> { + ssip.send(SSIPRequest::SetPriority(priority)).await?; + ssip.send(SSIPRequest::Speak).await?; + ssip.send(SSIPRequest::SendLines(Vec::from([text]))).await?; + Ok(()) +} + +#[tracing::instrument(ret)] +async fn doc_loaded(loaded: ActiveAppEvent) -> impl TryIntoCommands { + (Priority::Text, "Doc loaded") +} + +use crate::tower::state_changed::{Focused, Unfocused}; + +#[tracing::instrument(ret)] +async fn focused(state_changed: CacheEvent) -> impl TryIntoCommands { + Ok(vec![ + Focus(state_changed.item.object).into(), + Speak(state_changed.item.text, Priority::Text).into(), + ]) +} + +#[tracing::instrument(ret)] +async fn unfocused(state_changed: CacheEvent) -> impl TryIntoCommands { + Ok(vec![ + Focus(state_changed.item.object).into(), + Speak(state_changed.item.text, Priority::Text).into(), + ]) +} + +#[tracing::instrument(ret, err)] +async fn new_focused_item( + Command(Focus(new_focus)): Command, + AccessibleHistory(old_focus): AccessibleHistory, +) -> Result<(), OdiliaError> { + let _ = old_focus.lock()?.push(new_focus); + Ok(()) +} + +#[tracing::instrument(ret, err)] +async fn new_caret_pos( + Command(CaretPos(new_pos)): Command, + CurrentCaretPos(pos): CurrentCaretPos, +) -> Result<(), OdiliaError> { + pos.store(new_pos, core::sync::atomic::Ordering::Relaxed); + Ok(()) +} + +#[tracing::instrument(ret, err)] +async fn caret_moved( + caret_moved: CacheEvent, + LastCaretPos(last_pos): LastCaretPos, + LastFocused(last_focus): LastFocused, +) -> Result, OdiliaError> { + let mut commands: Vec = + vec![CaretPos(caret_moved.inner.position.try_into()?).into()]; + + if last_focus == caret_moved.item.object { + let start = min(caret_moved.inner.position.try_into()?, last_pos); + let end = max(caret_moved.inner.position.try_into()?, last_pos); + if let Some(text) = caret_moved.item.text.get(start..end) { + commands.extend((Priority::Text, text.to_string()).into_commands()); + } else { + return Err(OdiliaError::Generic(format!( + "Slide {}..{} could not be created from {}", + start, end, caret_moved.item.text + ))); + } + } else { + let (text, _, _) = caret_moved + .item + .get_string_at_offset( + caret_moved.inner.position.try_into()?, + Granularity::Line, + ) + .await?; + commands.extend((Priority::Text, text).into_commands()); + } + Ok(commands) +} + +#[tokio::main] async fn main() -> eyre::Result<()> { let args = Args::parse(); @@ -104,13 +208,12 @@ async fn main() -> eyre::Result<()> { tracing::error!("Could not set AT-SPI2 IsEnabled property because: {}", e); } let (sr_event_tx, sr_event_rx) = mpsc::channel(128); - // this channel must NEVER fill up; it will cause the thread receiving events to deadlock due to a zbus design choice. - // If you need to make it bigger, then make it bigger, but do NOT let it ever fill up. - let (atspi_event_tx, atspi_event_rx) = mpsc::channel(128); // this is the channel which handles all SSIP commands. If SSIP is not allowed to operate on a separate task, then waiting for the receiving message can block other long-running operations like structural navigation. // Although in the future, this may possibly be resolved through a proper cache, I think it still makes sense to separate SSIP's IO operations to a separate task. // Like the channel above, it is very important that this is *never* full, since it can cause deadlocking if the other task sending the request is working with zbus. let (ssip_req_tx, ssip_req_rx) = mpsc::channel::(128); + let (mut ev_tx, ev_rx) = + futures::channel::mpsc::channel::>(10_000); // Initialize state let state = Arc::new(ScreenReaderState::new(ssip_req_tx, config).await?); let ssip = odilia_tts::create_ssip_client().await?; @@ -133,15 +236,27 @@ async fn main() -> eyre::Result<()> { state.add_cache_match_rule(), )?; + // load handlers + let handlers = Handlers::new(state.clone()) + .command_listener(speak) + .command_listener(new_focused_item) + .command_listener(new_caret_pos) + .atspi_listener(doc_loaded) + .atspi_listener(caret_moved) + .atspi_listener(focused) + .atspi_listener(unfocused); + let ssip_event_receiver = odilia_tts::handle_ssip_commands(ssip, ssip_req_rx, token.clone()) .map(|r| r.wrap_err("Could no process SSIP request")); - let atspi_event_receiver = - events::receive(Arc::clone(&state), atspi_event_tx, token.clone()) - .map(|()| Ok::<_, eyre::Report>(())); - let atspi_event_processor = - events::process(Arc::clone(&state), atspi_event_rx, token.clone()) - .map(|()| Ok::<_, eyre::Report>(())); + /* + let atspi_event_receiver = + events::receive(Arc::clone(&state), atspi_event_tx, token.clone()) + .map(|()| Ok::<_, eyre::Report>(())); + let atspi_event_processor = + events::process(Arc::clone(&state), atspi_event_rx, token.clone()) + .map(|()| Ok::<_, eyre::Report>(())); + */ let odilia_event_receiver = sr_event_receiver(sr_event_tx, token.clone()) .map(|r| r.wrap_err("Could not process Odilia events")); let odilia_event_processor = @@ -149,13 +264,32 @@ async fn main() -> eyre::Result<()> { .map(|r| r.wrap_err("Could not process Odilia event")); let notification_task = notifications_monitor(Arc::clone(&state), token.clone()) .map(|r| r.wrap_err("Could not process signal shutdown.")); + let mut stream = state.atspi.event_stream(); + // There is a reason we are not reading from the event stream directly. + // This `MessageStream` can only store 64 events in its buffer. + // And, even if it could store more (it can via options), `zbus` specifically states that: + // > You must ensure a MessageStream is continuously polled or you will experience hangs. + // So, we continually poll it here, then receive it on the other end. + // Additioanlly, since sending is not async, but simply errors when there is an issue, this will + // help us avoid hangs. + let event_send_task = async move { + std::pin::pin!(&mut stream); + while let Some(ev) = stream.next().await { + if let Err(e) = ev_tx.try_send(ev) { + tracing::error!("Error sending event across channel! {e:?}"); + } + } + }; + let atspi_handlers_task = handlers.atspi_handler(ev_rx); - tracker.spawn(atspi_event_receiver); - tracker.spawn(atspi_event_processor); + //tracker.spawn(atspi_event_receiver); + //tracker.spawn(atspi_event_processor); tracker.spawn(odilia_event_receiver); tracker.spawn(odilia_event_processor); tracker.spawn(ssip_event_receiver); tracker.spawn(notification_task); + tracker.spawn(atspi_handlers_task); + tracker.spawn(event_send_task); tracker.close(); let _ = sigterm_signal_watcher(token, tracker) .await diff --git a/odilia/src/state.rs b/odilia/src/state.rs index 8237cac0..f41a8544 100644 --- a/odilia/src/state.rs +++ b/odilia/src/state.rs @@ -1,10 +1,15 @@ -use std::sync::atomic::AtomicUsize; +use std::{fmt::Debug, sync::atomic::AtomicUsize}; +use crate::tower::from_state::TryFromState; use circular_queue::CircularQueue; use eyre::WrapErr; +use futures::future::err; +use futures::future::ok; +use futures::future::Ready; use ssip_client_async::{MessageScope, Priority, PunctuationMode, Request as SSIPRequest}; -use tokio::sync::{mpsc::Sender, Mutex}; -use tracing::{debug, Instrument}; +use std::sync::Mutex; +use tokio::sync::mpsc::Sender; +use tracing::{debug, Instrument, Level}; use zbus::{fdo::DBusProxy, names::BusName, zvariant::ObjectPath, MatchRule, MessageType}; use atspi_common::{ @@ -14,9 +19,11 @@ use atspi_common::{ use atspi_connection::AccessibilityConnection; use atspi_proxies::{accessible::AccessibleProxy, cache::CacheProxy}; use odilia_cache::Convertable; -use odilia_cache::{AccessibleExt, AccessiblePrimitive, Cache, CacheItem}; +use odilia_cache::{AccessibleExt, Cache, CacheItem}; use odilia_common::{ - errors::CacheError, + cache::AccessiblePrimitive, + command::CommandType, + errors::{CacheError, OdiliaError}, modes::ScreenReaderMode, settings::{speech::PunctuationSpellingMode, ApplicationConfig}, types::TextSelectionArea, @@ -25,16 +32,113 @@ use odilia_common::{ use std::sync::Arc; #[allow(clippy::module_name_repetitions)] -pub struct ScreenReaderState { +pub(crate) struct ScreenReaderState { pub atspi: AccessibilityConnection, pub dbus: DBusProxy<'static>, pub ssip: Sender, - pub previous_caret_position: AtomicUsize, + pub previous_caret_position: Arc, pub mode: Mutex, - pub accessible_history: Mutex>, + pub accessible_history: Arc>>, pub event_history: Mutex>, pub cache: Arc, } +#[derive(Debug, Clone)] +pub struct AccessibleHistory(pub Arc>>); + +impl TryFromState, C> for AccessibleHistory { + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _cmd: C) -> Self::Future { + ok(AccessibleHistory(Arc::clone(&state.accessible_history))) + } +} +impl TryFromState, C> for CurrentCaretPos { + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _cmd: C) -> Self::Future { + ok(CurrentCaretPos(Arc::clone(&state.previous_caret_position))) + } +} + +#[derive(Debug, Clone)] +pub struct LastFocused(pub AccessiblePrimitive); +#[derive(Debug)] +pub struct CurrentCaretPos(pub Arc); +#[derive(Debug, Clone)] +pub struct LastCaretPos(pub usize); +pub struct Speech(pub Sender); +#[derive(Debug)] +pub struct Command(pub T) +where + T: CommandType; + +impl TryFromState, C> for Command +where + C: CommandType + Clone + Debug, +{ + type Error = OdiliaError; + type Future = Ready, Self::Error>>; + fn try_from_state(_state: Arc, cmd: C) -> Self::Future { + ok(Command(cmd)) + } +} + +impl TryFromState, C> for Speech +where + C: CommandType + Debug, +{ + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _cmd: C) -> Self::Future { + ok(Speech(state.ssip.clone())) + } +} + +impl TryFromState, E> for LastCaretPos +where + E: Debug, +{ + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _event: E) -> Self::Future { + ok(LastCaretPos( + state.previous_caret_position + .load(core::sync::atomic::Ordering::Relaxed), + )) + } +} + +impl TryFromState, E> for LastFocused +where + E: Debug, +{ + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _event: E) -> Self::Future { + let span = tracing::span!(Level::INFO, "try_from_state"); + let _enter = span.enter(); + let Ok(ml) = state.accessible_history.lock() else { + let e = OdiliaError::Generic("Could not get a lock on the history mutex. This is usually due to memory corruption or degradation and is a fatal error.".to_string()); + tracing::error!("{e:?}"); + return err(e); + }; + let Some(last) = ml.iter().nth(0).cloned() else { + let e = OdiliaError::Generic( + "There are no previously focused items.".to_string(), + ); + tracing::error!("{e:?}"); + return err(e); + }; + ok(LastFocused(last)) + } +} + +enum ConfigType { + CliOverride, + XDGConfigHome, + Etc, + CreateDefault, +} impl ScreenReaderState { #[tracing::instrument(skip_all)] @@ -57,8 +161,8 @@ impl ScreenReaderState { tracing::debug!("Reading configuration"); - let previous_caret_position = AtomicUsize::new(0); - let accessible_history = Mutex::new(CircularQueue::with_capacity(16)); + let previous_caret_position = Arc::new(AtomicUsize::new(0)); + let accessible_history = Arc::new(Mutex::new(CircularQueue::with_capacity(16))); let event_history = Mutex::new(CircularQueue::with_capacity(16)); let cache = Arc::new(Cache::new(atspi.connection().clone())); ssip.send(SSIPRequest::SetPitch( @@ -151,11 +255,11 @@ impl ScreenReaderState { &self, event: &T, ) -> OdiliaResult { - let prim = AccessiblePrimitive::from_event(event)?; + let prim = AccessiblePrimitive::from_event(event); if self.cache.get(&prim).is_none() { self.cache.add(CacheItem::from_atspi_event( event, - Arc::downgrade(&Arc::clone(&self.cache)), + Arc::clone(&self.cache), self.atspi.connection(), ) .await?)?; @@ -212,7 +316,7 @@ impl ScreenReaderState { // TODO: add logic for punctuation Ok(text_selection) } - #[tracing::instrument(skip_all)] + #[tracing::instrument(skip_all, err)] pub async fn register_event( &self, ) -> OdiliaResult<()> { @@ -260,25 +364,27 @@ impl ScreenReaderState { } #[allow(dead_code)] - pub async fn event_history_item(&self, index: usize) -> Option { - let history = self.event_history.lock().await; + pub fn event_history_item(&self, index: usize) -> Option { + let history = self.event_history.lock().ok()?; history.iter().nth(index).cloned() } - pub async fn event_history_update(&self, event: Event) { - let mut history = self.event_history.lock().await; - history.push(event); + pub fn event_history_update(&self, event: Event) { + if let Ok(mut history) = self.event_history.lock() { + history.push(event); + } } - pub async fn history_item<'a>(&self, index: usize) -> Option { - let history = self.accessible_history.lock().await; + pub fn history_item(&self, index: usize) -> Option { + let history = self.accessible_history.lock().ok()?; history.iter().nth(index).cloned() } /// Adds a new accessible to the history. We only store 16 previous accessibles, but theoretically, it should be lower. - pub async fn update_accessible(&self, new_a11y: AccessiblePrimitive) { - let mut history = self.accessible_history.lock().await; - history.push(new_a11y); + pub fn update_accessible(&self, new_a11y: AccessiblePrimitive) { + if let Ok(mut history) = self.accessible_history.lock() { + history.push(new_a11y); + } } pub async fn build_cache<'a, T>(&self, dest: T) -> OdiliaResult> where @@ -304,7 +410,7 @@ impl ScreenReaderState { .build() .await?; self.cache - .get_or_create(&accessible_proxy, Arc::downgrade(&self.cache)) + .get_or_create(&accessible_proxy, Arc::clone(&self.cache)) .await } #[tracing::instrument(skip_all, ret, err)] diff --git a/odilia/src/tower/README.md b/odilia/src/tower/README.md new file mode 100644 index 00000000..7b4bc4da --- /dev/null +++ b/odilia/src/tower/README.md @@ -0,0 +1,45 @@ +# SRaaS (Screen Reader as a Service) + +This document describes in some moderate detail both _why_ we chose to use `tower` (the Rust service-based architecture) for the infrastructure of Odilia, and also _how_ to contribute to various parts of the system. + +## Why + +Think about screen readers, what do they even do? + +Well, for the most part, they receive events from the system (user input, accessibility events), and then produce output for the system (speak text, update braille display, synthesize user input events). +All of these are things which _should_ be performed asyncronously (i.e., concurrently) since it is all based around IO. + +Since `tower` is a generic service/layer system for dealing with these kinds of asyncronous handling at various levels, let's explore the current architecture of Odilia and what kind of services we have created to deal with the unique challenges that a screen reader has to face. +TODO + +``` +Current tree of services: +TryIntoCommands is a trait which means it can be converted into Result, Error> + +TryInto(Event) -> Result + CacheLayer(E) -> (E, Arc) + AsyncTryInto(E, Arc) -> CacheEvent + Handler(CacheEvent, State): + fn(CacheEvnet, ...impl async TryFromState) + -> O: TryIntoCommand + for cmd in O -> Resul, Result>: + run_command(cmd) + +Desired tree: + +TryInto(Event) -> Result + StateLayer(E), -> (E, Arc) + AsyncTryInto(E, Arc) -> (CacheEvemt, ...impl async TryFromState) + Handler(CacheEvemt, ...impl async TryFromState) -> O: TryIntoCommands + for cmd in O::try_into_commands(): + run_command(cmd) + +This is more generic since it can convert _any_ type which needs state, including the cache, or any other part, for whatever reason. +This also makes it more complicated because `CacheEvent` needs to actually pass `E` and state into a conversion function to work, whereas some types can be converted without passing any additional information into the state for lookup. + +It gets a little complicated here with variable argument lists. Let's hope we can find the solution. +``` + +## TODOs + +- [ ] Document all the various services and layers and why they are there. diff --git a/odilia/src/tower/async_try.rs b/odilia/src/tower/async_try.rs new file mode 100644 index 00000000..386b8f23 --- /dev/null +++ b/odilia/src/tower/async_try.rs @@ -0,0 +1,110 @@ +#![allow(clippy::module_name_repetitions)] + +use crate::tower::from_state::TryFromState; +use futures::TryFutureExt; +use odilia_common::errors::OdiliaError; +use std::{ + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower::{Layer, Service}; + +impl AsyncTryFrom<(S, T)> for U +where + U: TryFromState, +{ + type Error = U::Error; + type Future = U::Future; + fn try_from_async(value: (S, T)) -> Self::Future { + U::try_from_state(value.0, value.1) + } +} + +pub trait AsyncTryFrom: Sized { + type Error; + type Future: Future>; + + fn try_from_async(value: T) -> Self::Future; +} +pub trait AsyncTryInto: Sized { + type Error; + type Future: Future>; + + fn try_into_async(self) -> Self::Future; +} +impl> AsyncTryInto for T { + type Error = U::Error; + type Future = U::Future; + fn try_into_async(self: T) -> Self::Future { + U::try_from_async(self) + } +} + +pub struct AsyncTryIntoService, S, R, Fut1> { + inner: S, + _marker: PhantomData R>, +} +impl, S, R, Fut1> AsyncTryIntoService { + pub fn new(inner: S) -> Self { + AsyncTryIntoService { inner, _marker: PhantomData } + } +} +pub struct AsyncTryIntoLayer> { + _marker: PhantomData O>, +} +impl> Clone for AsyncTryIntoLayer { + fn clone(&self) -> Self { + AsyncTryIntoLayer { _marker: PhantomData } + } +} +impl> AsyncTryIntoLayer { + pub fn new() -> Self { + AsyncTryIntoLayer { _marker: PhantomData } + } +} + +impl, O, S, Fut1> Layer for AsyncTryIntoLayer +where + S: Service, +{ + type Service = AsyncTryIntoService>::Response, Fut1>; + fn layer(&self, inner: S) -> Self::Service { + AsyncTryIntoService::new(inner) + } +} + +impl, S, R, Fut1> Clone for AsyncTryIntoService +where + S: Clone, +{ + fn clone(&self) -> Self { + AsyncTryIntoService { inner: self.inner.clone(), _marker: PhantomData } + } +} + +impl, S, R, Fut1> Service for AsyncTryIntoService +where + I: AsyncTryInto, + E: Into, + E2: Into, + S: Service + Clone, + Fut1: Future>, +{ + type Response = R; + type Future = impl Future>; + type Error = OdiliaError; + fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: I) -> Self::Future { + let clone = self.inner.clone(); + let mut inner = std::mem::replace(&mut self.inner, clone); + async move { + match req.try_into_async().await { + Ok(resp) => inner.call(resp).err_into().await, + Err(e) => Err(e.into()), + } + } + } +} diff --git a/odilia/src/tower/cache_event.rs b/odilia/src/tower/cache_event.rs new file mode 100644 index 00000000..ab996701 --- /dev/null +++ b/odilia/src/tower/cache_event.rs @@ -0,0 +1,132 @@ +use crate::{tower::from_state::TryFromState, OdiliaError, ScreenReaderState}; +use atspi_common::EventProperties; +use derived_deref::{Deref, DerefMut}; +use odilia_cache::CacheItem; +use odilia_common::cache::AccessiblePrimitive; +use refinement::Predicate; +use std::fmt::Debug; +use std::{future::Future, marker::PhantomData, sync::Arc}; +use zbus::{names::UniqueName, zvariant::ObjectPath}; + +pub type CacheEvent = CacheEventPredicate; +pub type ActiveAppEvent = CacheEventPredicate; + +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct CacheEventInner { + #[target] + pub inner: E, + pub item: CacheItem, +} +impl CacheEventInner +where + E: EventProperties + Debug, +{ + fn new(inner: E, item: CacheItem) -> Self { + Self { inner, item } + } +} + +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct CacheEventPredicate< + E: EventProperties + Debug, + P: Predicate<(E, Arc)>, +> { + #[target] + pub inner: E, + pub item: CacheItem, + _marker: PhantomData

, +} +impl CacheEventPredicate +where + E: EventProperties + Debug + Clone, + P: Predicate<(E, Arc)>, +{ + pub fn from_cache_event( + ce: CacheEventInner, + state: Arc, + ) -> Option { + if P::test(&(ce.inner.clone(), state)) { + return Some(Self { inner: ce.inner, item: ce.item, _marker: PhantomData }); + } + None + } +} + +#[derive(Debug)] +pub struct Always; +impl Predicate<(E, Arc)> for Always { + fn test(_: &(E, Arc)) -> bool { + true + } +} + +#[derive(Debug)] +pub struct ActiveApplication; +impl Predicate<(E, Arc)> for ActiveApplication +where + E: EventProperties, +{ + fn test((ev, state): &(E, Arc)) -> bool { + let Some(last_focused) = state.history_item(0) else { + return false; + }; + last_focused == ev.object_ref().into() + } +} + +impl TryFromState, E> for CacheEventInner +where + E: EventProperties + Debug + Clone, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state), ret)] + fn try_from_state(state: Arc, event: E) -> Self::Future { + async move { + let a11y = AccessiblePrimitive::from_event(&event); + let proxy = a11y.into_accessible(state.connection()).await?; + let cache_item = + state.cache.get_or_create(&proxy, Arc::clone(&state.cache)).await?; + Ok(CacheEventInner::new(event, cache_item)) + } + } +} + +impl TryFromState, E> for CacheEventPredicate +where + E: EventProperties + Debug + Clone, + P: Predicate<(E, Arc)> + Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state), ret)] + fn try_from_state(state: Arc, event: E) -> Self::Future { + async move { + let a11y = AccessiblePrimitive::from_event(&event); + let proxy = a11y.into_accessible(state.connection()).await?; + let cache_item = + state.cache.get_or_create(&proxy, Arc::clone(&state.cache)).await?; + let cache_event = CacheEventInner::new(event.clone(), cache_item); + CacheEventPredicate::from_cache_event(cache_event, state).ok_or( + OdiliaError::PredicateFailure(format!( + "Predicate cache event {} failed for event {:?}", + std::any::type_name::

(), + event + )), + ) + } + } +} + +impl EventProperties for CacheEventPredicate +where + E: EventProperties + Debug, + P: Predicate<(E, Arc)>, +{ + fn path(&self) -> ObjectPath<'_> { + self.inner.path() + } + fn sender(&self) -> UniqueName<'_> { + self.inner.sender() + } +} diff --git a/odilia/src/tower/choice.rs b/odilia/src/tower/choice.rs new file mode 100644 index 00000000..96f0e57e --- /dev/null +++ b/odilia/src/tower/choice.rs @@ -0,0 +1,126 @@ +use atspi_common::{BusProperties, Event, EventTypeProperties}; +use futures::future::err; +use futures::future::Either; +use futures::TryFutureExt; +use odilia_common::{ + command::{ + CommandType, CommandTypeDynamic, OdiliaCommand as Command, + OdiliaCommandDiscriminants as CommandDiscriminants, + }, + errors::OdiliaError, +}; +use std::collections::{btree_map::Entry, BTreeMap}; +use std::fmt::Debug; +use std::future::Future; +use std::marker::PhantomData; +use std::task::{Context, Poll}; +use tower::Service; + +pub trait Chooser { + fn identifier(&self) -> K; +} +pub trait ChooserStatic { + fn identifier() -> K; +} + +#[allow(clippy::module_name_repetitions)] +pub struct ChoiceService +where + S: Service, + Req: Chooser, +{ + services: BTreeMap, + _marker: PhantomData, +} + +impl Clone for ChoiceService +where + K: Clone, + S: Clone + Service, + Req: Chooser, +{ + fn clone(&self) -> Self { + ChoiceService { services: self.services.clone(), _marker: PhantomData } + } +} + +impl ChoiceService +where + S: Service, + Req: Chooser, +{ + pub fn new() -> Self { + ChoiceService { services: BTreeMap::new(), _marker: PhantomData } + } + pub fn insert(&mut self, k: K, s: S) + where + K: Ord, + { + self.services.insert(k, s); + } + pub fn entry(&mut self, k: K) -> Entry + where + K: Ord, + { + self.services.entry(k) + } +} + +impl Service for ChoiceService +where + S: Service + Clone, + Req: Chooser, + K: Ord + Debug, + OdiliaError: From, +{ + type Response = S::Response; + type Error = OdiliaError; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + for (_k, svc) in &mut self.services.iter_mut() { + let _ = svc.poll_ready(cx)?; + } + Poll::Ready(Ok(())) + } + fn call(&mut self, req: Req) -> Self::Future { + let k = req.identifier(); + + let mut svc = if let Some(orig_svc) = self.services.get_mut(&k) { + let clone = orig_svc.clone(); + std::mem::replace(orig_svc, clone) + } else { + return Either::Left(err(OdiliaError::ServiceNotFound( + format!("A service with key {k:?} could not be found in a list with keys of {:?}", self.services.keys()) + ))); + }; + Either::Right(svc.call(req).err_into()) + } +} + +impl ChooserStatic<(&'static str, &'static str)> for E +where + E: BusProperties, +{ + fn identifier() -> (&'static str, &'static str) { + (E::DBUS_INTERFACE, E::DBUS_MEMBER) + } +} +impl ChooserStatic for C +where + C: CommandType, +{ + fn identifier() -> CommandDiscriminants { + C::CTYPE + } +} + +impl Chooser<(&'static str, &'static str)> for Event { + fn identifier(&self) -> (&'static str, &'static str) { + (self.interface(), self.member()) + } +} +impl Chooser for Command { + fn identifier(&self) -> CommandDiscriminants { + self.ctype() + } +} diff --git a/odilia/src/tower/from_state.rs b/odilia/src/tower/from_state.rs new file mode 100644 index 00000000..322a0c7d --- /dev/null +++ b/odilia/src/tower/from_state.rs @@ -0,0 +1,67 @@ +#![allow(clippy::module_name_repetitions)] + +use futures::FutureExt; +use futures_concurrency::future::Join; + +use odilia_common::errors::OdiliaError; +use std::fmt::Debug; +use std::future::Future; + +pub trait TryFromState: Sized { + type Error; + type Future: Future>; + fn try_from_state(state: S, data: T) -> Self::Future; +} + +impl TryFromState for (U1,) +where + U1: TryFromState, + OdiliaError: From, + T: Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state))] + fn try_from_state(state: S, data: T) -> Self::Future { + (U1::try_from_state(state, data),).join().map(|(u1,)| Ok((u1?,))) + } +} +impl TryFromState for (U1, U2) +where + U1: TryFromState, + U2: TryFromState, + OdiliaError: From + From, + S: Clone, + T: Clone + Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state))] + fn try_from_state(state: S, data: T) -> Self::Future { + (U1::try_from_state(state.clone(), data.clone()), U2::try_from_state(state, data)) + .join() + .map(|(u1, u2)| Ok((u1?, u2?))) + } +} +impl TryFromState for (U1, U2, U3) +where + U1: TryFromState, + U2: TryFromState, + U3: TryFromState, + OdiliaError: From + From + From, + S: Clone, + T: Clone + Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state))] + fn try_from_state(state: S, data: T) -> Self::Future { + ( + U1::try_from_state(state.clone(), data.clone()), + U2::try_from_state(state.clone(), data.clone()), + U3::try_from_state(state, data), + ) + .join() + .map(|(u1, u2, u3)| Ok((u1?, u2?, u3?))) + } +} diff --git a/odilia/src/tower/handler.rs b/odilia/src/tower/handler.rs new file mode 100644 index 00000000..f4611a8c --- /dev/null +++ b/odilia/src/tower/handler.rs @@ -0,0 +1,85 @@ +#![allow(clippy::module_name_repetitions)] + +use futures::FutureExt; +use std::{ + convert::Infallible, + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower::Service; + +pub trait Handler { + type Response; + type Future: Future; + fn into_service(self) -> HandlerService + where + Self: Sized, + { + HandlerService::new(self) + } + fn call(self, params: T) -> Self::Future; +} + +macro_rules! impl_handler { + ($($type:ident,)+) => { + #[allow(non_snake_case)] + impl Handler<($($type,)+)> for F + where + F: FnOnce($($type,)+) -> Fut + Send, + Fut: Future + Send, + $($type: Send,)+ { + type Response = R; + type Future = impl Future; + fn call(self, params: ($($type,)+)) -> Self::Future { + let ($($type,)+) = params; + self($($type,)+) + } + } +} +} +impl_handler!(T1,); +impl_handler!(T1, T2,); +impl_handler!(T1, T2, T3,); +impl_handler!(T1, T2, T3, T4,); +impl_handler!(T1, T2, T3, T4, T5,); +impl_handler!(T1, T2, T3, T4, T5, T6,); +impl_handler!(T1, T2, T3, T4, T5, T6, T7,); + +#[allow(clippy::type_complexity)] +pub struct HandlerService { + handler: H, + _marker: PhantomData, +} +impl Clone for HandlerService +where + H: Clone, +{ + fn clone(&self) -> Self { + HandlerService { handler: self.handler.clone(), _marker: PhantomData } + } +} +impl HandlerService { + fn new(handler: H) -> Self + where + H: Handler, + { + HandlerService { handler, _marker: PhantomData } + } +} + +impl Service for HandlerService +where + H: Handler + Clone, +{ + type Response = H::Response; + type Future = impl Future>; + type Error = Infallible; + + fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, params: T) -> Self::Future { + self.handler.clone().call(params).map(Ok) + } +} diff --git a/odilia/src/tower/handlers.rs b/odilia/src/tower/handlers.rs new file mode 100644 index 00000000..283556c4 --- /dev/null +++ b/odilia/src/tower/handlers.rs @@ -0,0 +1,144 @@ +#![allow(dead_code)] + +use crate::state::ScreenReaderState; +use crate::tower::{ + choice::{ChoiceService, ChooserStatic}, + from_state::TryFromState, + service_set::ServiceSet, + Handler, ServiceExt as OdiliaServiceExt, +}; +use atspi::AtspiError; +use atspi::BusProperties; +use atspi::Event; +use atspi::EventProperties; +use atspi::EventTypeProperties; +use odilia_common::errors::OdiliaError; +use std::fmt::Debug; +use std::sync::Arc; + +use futures::{Stream, StreamExt}; + +use tower::util::BoxCloneService; +use tower::Service; +use tower::ServiceExt; + +use tokio::sync::mpsc::Receiver; + +use odilia_common::command::{ + CommandType, OdiliaCommand as Command, OdiliaCommandDiscriminants as CommandDiscriminants, + TryIntoCommands, +}; + +type Response = Vec; +type Request = Event; +type Error = OdiliaError; + +type AtspiHandler = BoxCloneService; +type CommandHandler = BoxCloneService; + +pub struct Handlers { + state: Arc, + atspi: ChoiceService<(&'static str, &'static str), ServiceSet, Event>, + command: ChoiceService, Command>, +} + +impl Handlers { + pub fn new(state: Arc) -> Self { + Handlers { state, atspi: ChoiceService::new(), command: ChoiceService::new() } + } + pub async fn command_handler(mut self, mut commands: Receiver) { + loop { + let maybe_cmd = commands.recv().await; + let Some(cmd) = maybe_cmd else { + tracing::error!("Error cmd: {maybe_cmd:?}"); + continue; + }; + // NOTE: Why not use join_all(...) ? + // Because this drives the futures concurrently, and we want ordered handlers. + // Otherwise, we cannot guarentee that the caching functions get run first. + // we could move caching to a separate, ordered system, then parallelize the other functions, + // if we determine this is a performance problem. + if let Err(e) = self.command.call(cmd).await { + tracing::error!("{e:?}"); + } + } + } + #[tracing::instrument(skip_all)] + pub async fn atspi_handler(mut self, mut events: R) + where + R: Stream> + Unpin, + { + std::pin::pin!(&mut events); + loop { + let maybe_ev = events.next().await; + let Some(Ok(ev)) = maybe_ev else { + tracing::error!("Error in processing {maybe_ev:?}"); + continue; + }; + if let Err(e) = self.atspi.call(ev).await { + tracing::error!("{e:?}"); + } + } + } + pub fn command_listener(mut self, handler: H) -> Self + where + H: Handler + Send + Clone + 'static, + >::Future: Send, + C: CommandType + ChooserStatic + Send + 'static, + Command: TryInto, + OdiliaError: From<>::Error> + + From<, C>>::Error>, + R: Into> + Send + 'static, + T: TryFromState, C> + Send + 'static, + , C>>::Future: Send, + , C>>::Error: Send, + { + let bs = handler + .into_service() + .unwrap_map(Into::into) + .request_async_try_from() + .with_state(Arc::clone(&self.state)) + .request_try_from() + .boxed_clone(); + self.command.entry(C::identifier()).or_default().push(bs); + Self { state: self.state, atspi: self.atspi, command: self.command } + } + pub fn atspi_listener(mut self, handler: H) -> Self + where + H: Handler + Send + Clone + 'static, + >::Future: Send, + E: EventTypeProperties + + Debug + + BusProperties + + TryFrom + + EventProperties + + ChooserStatic<(&'static str, &'static str)> + + Clone + + Send + + 'static, + OdiliaError: From<>::Error> + + From<, E>>::Error>, + R: TryIntoCommands + 'static, + T: TryFromState, E> + Send + 'static, + , E>>::Error: Send + 'static, + , E>>::Future: Send, + { + let bs = handler + .into_service() + .unwrap_map(TryIntoCommands::try_into_commands) + .request_async_try_from() + .with_state(Arc::clone(&self.state)) + .request_try_from() + .iter_into(self.command.clone()) + .map_result( + |res: Result>>, OdiliaError>| { + res?.into_iter() + .flatten() + .collect::>() + }, + ) + .boxed_clone(); + self.atspi.entry(E::identifier()).or_default().push(bs); + Self { state: self.state, atspi: self.atspi, command: self.command } + } +} diff --git a/odilia/src/tower/iter_svc.rs b/odilia/src/tower/iter_svc.rs new file mode 100644 index 00000000..a4ade66b --- /dev/null +++ b/odilia/src/tower/iter_svc.rs @@ -0,0 +1,66 @@ +use std::future::Future; +use std::marker::PhantomData; +use std::task::Context; +use std::task::Poll; +use tower::Service; + +#[allow(clippy::type_complexity)] +pub struct IterService { + inner: S1, + outer: S2, + _marker: PhantomData Result<(Iter, I), E>>, +} +impl Clone for IterService +where + S1: Clone, + S2: Clone, +{ + fn clone(&self) -> Self { + IterService { + inner: self.inner.clone(), + outer: self.outer.clone(), + _marker: PhantomData, + } + } +} +impl IterService +where + S1: Service, + Iter: IntoIterator, + S2: Service, +{ + pub fn new(inner: S1, outer: S2) -> Self { + IterService { inner, outer, _marker: PhantomData } + } +} + +impl Service for IterService +where + S1: Service + Clone, + Iter: IntoIterator, + S2: Service + Clone, + E: From + From, +{ + type Response = Vec; + type Error = E; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + let _ = self.inner.poll_ready(cx).map_err(Into::::into)?; + self.outer.poll_ready(cx).map_err(Into::into) + } + fn call(&mut self, input: Req) -> Self::Future { + let clone_outer = self.outer.clone(); + let mut outer = std::mem::replace(&mut self.outer, clone_outer); + let clone_inner = self.inner.clone(); + let mut inner = std::mem::replace(&mut self.inner, clone_inner); + async move { + let iter = inner.call(input).await?; + let mut results = vec![]; + for item in iter { + let result = outer.call(item).await?; + results.push(result); + } + Ok(results) + } + } +} diff --git a/odilia/src/tower/mod.rs b/odilia/src/tower/mod.rs new file mode 100644 index 00000000..52b5335d --- /dev/null +++ b/odilia/src/tower/mod.rs @@ -0,0 +1,18 @@ +pub mod async_try; +pub mod cache_event; +pub use cache_event::CacheEvent; +pub mod choice; +pub mod from_state; +pub mod handler; +pub mod iter_svc; +pub mod service_ext; +pub mod service_set; +pub mod state_changed; +pub mod state_svc; +pub mod sync_try; +pub mod unwrap_svc; +pub use handler::Handler; +pub use service_ext::ServiceExt; + +pub mod handlers; +pub use handlers::*; diff --git a/odilia/src/tower/service_ext.rs b/odilia/src/tower/service_ext.rs new file mode 100644 index 00000000..6a5a2e3e --- /dev/null +++ b/odilia/src/tower/service_ext.rs @@ -0,0 +1,52 @@ +use crate::tower::{ + async_try::{AsyncTryInto, AsyncTryIntoLayer, AsyncTryIntoService}, + iter_svc::IterService, + state_svc::{StateLayer, StateService}, + sync_try::{TryIntoLayer, TryIntoService}, + unwrap_svc::UnwrapService, +}; +use std::{convert::Infallible, sync::Arc}; +use tower::{Layer, Service}; + +pub trait ServiceExt: Service { + fn request_try_from(self) -> TryIntoService + where + Self: Sized, + I: TryInto, + Self: Service, + { + TryIntoLayer::new().layer(self) + } + fn request_async_try_from( + self, + ) -> AsyncTryIntoService + where + I: AsyncTryInto, + Self: Service + Clone, + { + AsyncTryIntoLayer::new().layer(self) + } + fn with_state(self, s: Arc) -> StateService + where + Self: Sized, + { + StateLayer::new(s).layer(self) + } + fn unwrap_map(self, f: F) -> UnwrapService + where + Self: Service + Sized, + F: FnOnce(>::Response) -> Result, + { + UnwrapService::new(self, f) + } + fn iter_into(self, s: S) -> IterService + where + Self: Service + Sized, + Iter: IntoIterator, + S: Service, + { + IterService::new(self, s) + } +} + +impl ServiceExt for T where T: Service {} diff --git a/odilia/src/tower/service_set.rs b/odilia/src/tower/service_set.rs new file mode 100644 index 00000000..29bb764d --- /dev/null +++ b/odilia/src/tower/service_set.rs @@ -0,0 +1,56 @@ +use std::future::Future; +use std::task::{Context, Poll}; +use tower::Service; + +/// A series of services which are executed in the order they are placed in the [`ServiceSet::new`] +/// initializer. +/// Useful when creating a set of handler functions that need to be run without concurrency. +/// +/// Note that although calling the [`ServiceSet::call`] function seems to return a +/// `Result, S::Error>`, the outer error is gaurenteed never to be +/// returned and can safely be unwrapped _from the caller function_. +#[derive(Clone)] +pub struct ServiceSet { + services: Vec, +} +impl Default for ServiceSet { + fn default() -> Self { + ServiceSet { services: vec![] } + } +} +impl ServiceSet { + pub fn new>(services: I) -> Self { + ServiceSet { services: services.into_iter().collect() } + } + pub fn push(&mut self, svc: S) { + self.services.push(svc); + } +} + +impl Service for ServiceSet +where + S: Service + Clone, + Req: Clone, +{ + type Response = Vec>; + type Error = S::Error; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + for svc in &mut self.services { + let _ = svc.poll_ready(cx)?; + } + Poll::Ready(Ok(())) + } + fn call(&mut self, req: Req) -> Self::Future { + let clone = self.services.clone(); + let services = std::mem::replace(&mut self.services, clone); + async move { + let mut results = vec![]; + for mut svc in services { + let result = svc.call(req.clone()).await; + results.push(result); + } + Ok(results) + } + } +} diff --git a/odilia/src/tower/state_changed.rs b/odilia/src/tower/state_changed.rs new file mode 100644 index 00000000..f832d88a --- /dev/null +++ b/odilia/src/tower/state_changed.rs @@ -0,0 +1,270 @@ +use atspi_common::{ + events::object::StateChangedEvent, AtspiError, EventProperties, State as AtspiState, +}; +use derived_deref::{Deref, DerefMut}; +use refinement::Predicate; +use std::marker::PhantomData; +use zbus::{names::UniqueName, zvariant::ObjectPath}; + +pub type Focused = StateChanged; +pub type Unfocused = StateChanged; + +#[derive(Debug, Default, Clone, Deref, DerefMut)] +pub struct StateChanged { + #[target] + ev: StateChangedEvent, + _marker: PhantomData<(S, E)>, +} +impl EventProperties for StateChanged { + fn sender(&self) -> UniqueName<'_> { + self.ev.sender() + } + fn path(&self) -> ObjectPath<'_> { + self.ev.path() + } +} +impl atspi::BusProperties for StateChanged +where + StateChanged: TryFrom, +{ + const DBUS_MEMBER: &'static str = StateChangedEvent::DBUS_MEMBER; + const DBUS_INTERFACE: &'static str = StateChangedEvent::DBUS_INTERFACE; + const MATCH_RULE_STRING: &'static str = StateChangedEvent::MATCH_RULE_STRING; + const REGISTRY_EVENT_STRING: &'static str = StateChangedEvent::REGISTRY_EVENT_STRING; + type Body = ::Body; + fn from_message_parts(or: atspi::ObjectRef, bdy: Self::Body) -> Result { + let ev = StateChangedEvent::from_message_parts(or, bdy)?; + // TODO: we do not have an appropriate event type here; this should really be an OdiliaError. + // We may want to consider adding a type Error in the BusProperties impl. + Self::try_from(ev).map_err(|_| AtspiError::InterfaceMatch(String::new())) + } + fn body(&self) -> Self::Body { + self.ev.body() + } +} + +impl TryFrom for StateChanged +where + S: Predicate, + E: Predicate, +{ + type Error = crate::OdiliaError; + fn try_from(ev: atspi::Event) -> Result { + let state_changed_ev: StateChangedEvent = ev.try_into()?; + StateChanged::::try_from(state_changed_ev) + } +} + +impl TryFrom for StateChanged +where + S: Predicate, + E: Predicate, +{ + type Error = crate::OdiliaError; + fn try_from(ev: StateChangedEvent) -> Result { + if >::test(&ev) { + Ok(Self { ev, _marker: PhantomData }) + } else { + Err(crate::OdiliaError::PredicateFailure(format!("The type {ev:?} is not compatible with the predicate requirements state = {:?} and enabled = {:?}", std::any::type_name::(), std::any::type_name::()))) + } + } +} + +impl Predicate for StateChanged +where + S: Predicate, + E: Predicate, +{ + fn test(ev: &StateChangedEvent) -> bool { + >::test(&ev.state) + && >::test(&ev.enabled) + } +} + +#[allow(unused)] +#[derive(Debug, Clone, Copy)] +pub struct True; +#[allow(unused)] +#[derive(Debug, Clone, Copy)] +pub struct False; + +impl Predicate for True { + fn test(b: &bool) -> bool { + *b + } +} +impl Predicate for False { + fn test(b: &bool) -> bool { + !*b + } +} + +macro_rules! impl_refinement_type { + ($enum:ty, $variant:expr, $name:ident) => { + #[allow(unused)] + #[derive(Debug, Clone, Copy)] + pub struct $name; + impl Predicate<$enum> for $name { + fn test(outer: &$enum) -> bool { + &$variant == outer + } + } + }; +} +#[allow(unused)] +pub struct AnyState; + +impl Predicate for AnyState { + fn test(outer: &AtspiState) -> bool { + match *outer { + AtspiState::Invalid => >::test(outer), + AtspiState::Active => >::test(outer), + AtspiState::Armed => >::test(outer), + AtspiState::Busy => >::test(outer), + AtspiState::Checked => >::test(outer), + AtspiState::Collapsed => { + >::test(outer) + } + AtspiState::Defunct => >::test(outer), + AtspiState::Editable => { + >::test(outer) + } + AtspiState::Enabled => >::test(outer), + AtspiState::Expandable => { + >::test(outer) + } + AtspiState::Expanded => { + >::test(outer) + } + AtspiState::Focusable => { + >::test(outer) + } + AtspiState::Focused => >::test(outer), + AtspiState::HasTooltip => { + >::test(outer) + } + AtspiState::Horizontal => { + >::test(outer) + } + AtspiState::Iconified => { + >::test(outer) + } + AtspiState::Modal => >::test(outer), + AtspiState::MultiLine => { + >::test(outer) + } + AtspiState::Multiselectable => { + >::test(outer) + } + AtspiState::Opaque => >::test(outer), + AtspiState::Pressed => >::test(outer), + AtspiState::Resizable => { + >::test(outer) + } + AtspiState::Selectable => { + >::test(outer) + } + AtspiState::Selected => { + >::test(outer) + } + AtspiState::Sensitive => { + >::test(outer) + } + AtspiState::Showing => >::test(outer), + AtspiState::SingleLine => { + >::test(outer) + } + AtspiState::Stale => >::test(outer), + AtspiState::Transient => { + >::test(outer) + } + AtspiState::Vertical => { + >::test(outer) + } + AtspiState::Visible => >::test(outer), + AtspiState::ManagesDescendants => { + >::test(outer) + } + AtspiState::Indeterminate => { + >::test(outer) + } + AtspiState::Required => { + >::test(outer) + } + AtspiState::Truncated => { + >::test(outer) + } + AtspiState::Animated => { + >::test(outer) + } + AtspiState::InvalidEntry => { + >::test(outer) + } + AtspiState::SupportsAutocompletion => { + >::test(outer) + } + AtspiState::SelectableText => { + >::test(outer) + } + AtspiState::IsDefault => { + >::test(outer) + } + AtspiState::Visited => >::test(outer), + AtspiState::Checkable => { + >::test(outer) + } + AtspiState::HasPopup => { + >::test(outer) + } + AtspiState::ReadOnly => { + >::test(outer) + } + _ => todo!(), + } + } +} + +impl_refinement_type!(AtspiState, AtspiState::Invalid, StateInvalid); +impl_refinement_type!(AtspiState, AtspiState::Active, StateActive); +impl_refinement_type!(AtspiState, AtspiState::Armed, StateArmed); +impl_refinement_type!(AtspiState, AtspiState::Busy, StateBusy); +impl_refinement_type!(AtspiState, AtspiState::Checked, StateChecked); +impl_refinement_type!(AtspiState, AtspiState::Collapsed, StateCollapsed); +impl_refinement_type!(AtspiState, AtspiState::Defunct, StateDefunct); +impl_refinement_type!(AtspiState, AtspiState::Editable, StateEditable); +impl_refinement_type!(AtspiState, AtspiState::Enabled, StateEnabled); +impl_refinement_type!(AtspiState, AtspiState::Expandable, StateExpandable); +impl_refinement_type!(AtspiState, AtspiState::Expanded, StateExpanded); +impl_refinement_type!(AtspiState, AtspiState::Focusable, StateFocusable); +impl_refinement_type!(AtspiState, AtspiState::Focused, StateFocused); +impl_refinement_type!(AtspiState, AtspiState::HasTooltip, StateHasTooltip); +impl_refinement_type!(AtspiState, AtspiState::Horizontal, StateHorizontal); +impl_refinement_type!(AtspiState, AtspiState::Iconified, StateIconified); +impl_refinement_type!(AtspiState, AtspiState::Modal, StateModal); +impl_refinement_type!(AtspiState, AtspiState::MultiLine, StateMultiLine); +impl_refinement_type!(AtspiState, AtspiState::Multiselectable, StateMultiselectable); +impl_refinement_type!(AtspiState, AtspiState::Opaque, StateOpaque); +impl_refinement_type!(AtspiState, AtspiState::Pressed, StatePressed); +impl_refinement_type!(AtspiState, AtspiState::Resizable, StateResizable); +impl_refinement_type!(AtspiState, AtspiState::Selectable, StateSelectable); +impl_refinement_type!(AtspiState, AtspiState::Selected, StateSelected); +impl_refinement_type!(AtspiState, AtspiState::Sensitive, StateSensitive); +impl_refinement_type!(AtspiState, AtspiState::Showing, StateShowing); +impl_refinement_type!(AtspiState, AtspiState::SingleLine, StateSingleLine); +impl_refinement_type!(AtspiState, AtspiState::Stale, StateStale); +impl_refinement_type!(AtspiState, AtspiState::Transient, StateTransient); +impl_refinement_type!(AtspiState, AtspiState::Vertical, StateVertical); +impl_refinement_type!(AtspiState, AtspiState::Visible, StateVisible); +impl_refinement_type!(AtspiState, AtspiState::ManagesDescendants, StateManagesDescendants); +impl_refinement_type!(AtspiState, AtspiState::Indeterminate, StateIndeterminate); +impl_refinement_type!(AtspiState, AtspiState::Required, StateRequired); +impl_refinement_type!(AtspiState, AtspiState::Truncated, StateTruncated); +impl_refinement_type!(AtspiState, AtspiState::Animated, StateAnimated); +impl_refinement_type!(AtspiState, AtspiState::InvalidEntry, StateInvalidEntry); +impl_refinement_type!(AtspiState, AtspiState::SupportsAutocompletion, StateSupportsAutocompletion); +impl_refinement_type!(AtspiState, AtspiState::SelectableText, StateSelectableText); +impl_refinement_type!(AtspiState, AtspiState::IsDefault, StateIsDefault); +impl_refinement_type!(AtspiState, AtspiState::Visited, StateVisited); +impl_refinement_type!(AtspiState, AtspiState::Checkable, StateCheckable); +impl_refinement_type!(AtspiState, AtspiState::HasPopup, StateHasPopup); +impl_refinement_type!(AtspiState, AtspiState::ReadOnly, StateReadOnly); diff --git a/odilia/src/tower/state_svc.rs b/odilia/src/tower/state_svc.rs new file mode 100644 index 00000000..c3d3db22 --- /dev/null +++ b/odilia/src/tower/state_svc.rs @@ -0,0 +1,49 @@ +use std::{ + sync::Arc, + task::{Context, Poll}, +}; +use tower::{Layer, Service}; + +pub struct StateLayer { + state: Arc, +} +impl StateLayer { + pub fn new(state: Arc) -> Self { + StateLayer { state } + } +} + +pub struct StateService { + inner: Srv, + state: Arc, +} +impl Clone for StateService +where + Srv: Clone, +{ + fn clone(&self) -> Self { + StateService { inner: self.inner.clone(), state: Arc::clone(&self.state) } + } +} + +impl Service for StateService +where + Srv: Service<(Arc, I)>, +{ + type Error = Srv::Error; + type Response = Srv::Response; + type Future = Srv::Future; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + fn call(&mut self, input: I) -> Self::Future { + self.inner.call((Arc::clone(&self.state), input)) + } +} + +impl Layer for StateLayer { + type Service = StateService; + fn layer(&self, inner: Srv) -> Self::Service { + StateService { inner, state: Arc::clone(&self.state) } + } +} diff --git a/odilia/src/tower/sync_try.rs b/odilia/src/tower/sync_try.rs new file mode 100644 index 00000000..892bb2e5 --- /dev/null +++ b/odilia/src/tower/sync_try.rs @@ -0,0 +1,65 @@ +use futures::future::{err, Either, Ready}; +use std::{ + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower::{Layer, Service}; + +pub struct TryIntoService, S, R, Fut1> { + inner: S, + _marker: PhantomData R>, +} +impl, S, R, Fut1> TryIntoService { + pub fn new(inner: S) -> Self { + TryIntoService { inner, _marker: PhantomData } + } +} +pub struct TryIntoLayer> { + _marker: PhantomData O>, +} +impl> TryIntoLayer { + pub fn new() -> Self { + TryIntoLayer { _marker: PhantomData } + } +} + +impl, O, S, Fut1> Layer for TryIntoLayer +where + S: Service, +{ + type Service = TryIntoService>::Response, Fut1>; + fn layer(&self, inner: S) -> Self::Service { + TryIntoService::new(inner) + } +} + +impl, S, R, Fut1> Clone for TryIntoService +where + S: Clone, +{ + fn clone(&self) -> Self { + TryIntoService { inner: self.inner.clone(), _marker: PhantomData } + } +} + +impl, S, R, Fut1> Service for TryIntoService +where + I: TryInto, + E: From<>::Error>, + S: Service, + Fut1: Future>, +{ + type Response = R; + type Future = Either>>; + type Error = E; + fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: I) -> Self::Future { + match req.try_into() { + Ok(o) => Either::Left(self.inner.call(o)), + Err(e) => Either::Right(err(e.into())), + } + } +} diff --git a/odilia/src/tower/unwrap_svc.rs b/odilia/src/tower/unwrap_svc.rs new file mode 100644 index 00000000..9a902b63 --- /dev/null +++ b/odilia/src/tower/unwrap_svc.rs @@ -0,0 +1,47 @@ +use futures::FutureExt; +use std::convert::Infallible; +use std::future::Future; +use std::marker::PhantomData; +use std::task::{Context, Poll}; +use tower::Service; + +#[allow(clippy::type_complexity)] +pub struct UnwrapService { + inner: S, + f: F, + _marker: PhantomData Result>, +} +impl UnwrapService +where + S: Service, +{ + pub fn new(inner: S, f: F) -> Self { + UnwrapService { inner, f, _marker: PhantomData } + } +} +impl Clone for UnwrapService +where + S: Clone, + F: Clone, +{ + fn clone(&self) -> Self { + UnwrapService { inner: self.inner.clone(), f: self.f.clone(), _marker: PhantomData } + } +} + +impl Service for UnwrapService +where + S: Service, + E: From, + F: FnOnce(S::Response) -> Result + Clone, +{ + type Error = E; + type Response = R; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Into::into) + } + fn call(&mut self, req: Req) -> Self::Future { + self.inner.call(req).map(|res| res.unwrap()).map(self.f.clone()) + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..af52e536 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2024-08-08" diff --git a/scripts/tokio_console_run.sh b/scripts/tokio_console_run.sh new file mode 100755 index 00000000..0e0f1918 --- /dev/null +++ b/scripts/tokio_console_run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +RUSTFLAGS="--cfg tokio_unstable" RUST_LOG="trace" cargo run --features tokio-console diff --git a/tts/Cargo.toml b/tts/Cargo.toml index bd7fce12..0cfa7b9b 100644 --- a/tts/Cargo.toml +++ b/tts/Cargo.toml @@ -11,8 +11,9 @@ categories = ["accessibility", "api-bindings"] edition = "2021" [dependencies] -ssip-client-async.workspace = true +ssip-client-async = { version = "0.13.0", features = ["tokio"] } tokio.workspace = true tokio-util.workspace=true tracing.workspace = true eyre.workspace = true +ssip = "0.1.0"