Discovery: Refactor and add Avahi DBus backend (#1347)

* discovery: use opaque error type for DnsSdError

This helps to decouple discovery and core by not leaking implementation
details of the zeroconf backend into Error conversion impls in core.

* discovery: map all MDNS/DNS-SD errors to DiscoveryError::DnsSdError

previously, libmdns errors would use a generic conversion
from std::io::Error to core::Error

* discovery: use an opaque type for the handle to the DNS-SD service

* discovery: make features additive

i.e. add with-libmdns instead of using not(with-dns-sd).

The logic is such that enabling with-dns-sd in addition to the default
with-libmdns will still end up using dns-sd, as before.
If only with-libmdns is enabled, that will be the default.
If none of the features is enabled, attempting to build a `Discovery`
will yield an error.

* discovery: add --zeroconf-backend CLI flag

* discovery: Add minimal Avahi zeroconf backend

* bump MSRV to 1.75

required by zbus >= 4

* discovery: ensure that server and dns-sd backend shutdown gracefully

Previously, on drop the the shutdown_tx/close_tx, it wasn't guaranteed
the corresponding tasks would continue to be polled until they actually
completed their shutdown.

Since dns_sd::Service is not Send and non-async, and because libmdns is
non-async, put them on their own threads.

* discovery: use a shared channel for server and zeroconf status messages

* discovery: add Avahi reconnection logic

This deals gracefully with the case where the Avahi daemon is restarted
or not running initially.

* discovery: allow running when compiled without zeroconf backend...

...but exit with an error if there's no way to authenticate

* better error messages for invalid options with no short flag
This commit is contained in:
Benedikt 2024-10-26 16:45:02 +02:00 committed by GitHub
parent d2324ddd1b
commit 94d174c33d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1156 additions and 129 deletions

View file

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG debian_version=slim-bookworm ARG debian_version=slim-bookworm
ARG rust_version=1.74.0 ARG rust_version=1.75.0
FROM rust:${rust_version}-${debian_version} FROM rust:${rust_version}-${debian_version}
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive

View file

@ -109,7 +109,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
toolchain: toolchain:
- "1.74" # MSRV (Minimum supported rust version) - "1.75" # MSRV (Minimum supported rust version)
- stable - stable
experimental: [false] experimental: [false]
# Ignore failures in beta # Ignore failures in beta
@ -164,7 +164,7 @@ jobs:
matrix: matrix:
os: [windows-latest] os: [windows-latest]
toolchain: toolchain:
- "1.74" # MSRV (Minimum supported rust version) - "1.75" # MSRV (Minimum supported rust version)
- stable - stable
steps: steps:
- name: Checkout code - name: Checkout code
@ -215,7 +215,7 @@ jobs:
- aarch64-unknown-linux-gnu - aarch64-unknown-linux-gnu
- riscv64gc-unknown-linux-gnu - riscv64gc-unknown-linux-gnu
toolchain: toolchain:
- "1.74" # MSRV (Minimum supported rust version) - "1.75" # MSRV (Minimum supported rust version)
- stable - stable
steps: steps:
- name: Checkout code - name: Checkout code

View file

@ -10,11 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- [core] The `access_token` for http requests is now acquired by `login5` - [core] The `access_token` for http requests is now acquired by `login5`
- [core] MSRV is now 1.75 (breaking)
- [discovery] librespot can now be compiled with multiple MDNS/DNS-SD backends
(avahi, dns_sd, libmdns) which can be selected using a CLI flag. The defaults
are unchanged (breaking).
### Added ### Added
- [core] Add `login` (mobile) and `auth_token` retrieval via login5 - [core] Add `login` (mobile) and `auth_token` retrieval via login5
- [core] Add `OS` and `os_version` to `config.rs` - [core] Add `OS` and `os_version` to `config.rs`
- [discovery] Added a new MDNS/DNS-SD backend which connects to Avahi via D-Bus.
### Removed ### Removed

View file

@ -56,6 +56,17 @@ On Fedora systems:
sudo dnf install alsa-lib-devel sudo dnf install alsa-lib-devel
``` ```
### Zeroconf library dependencies
Depending on the chosen backend, specific development libraries are required.
*_Note this is an non-exhaustive list, open a PR to add to it!_*
| Zeroconf backend | Debian/Ubuntu | Fedora | macOS |
|--------------------|------------------------------|-----------------------------------|-------------|
|avahi | | | |
|dns_sd | `libavahi-compat-libdnssd-dev pkg-config` | `avahi-compat-libdns_sd-devel` | |
|libmdns (default) | | | |
### Getting the Source ### Getting the Source
The recommended method is to first fork the repo, so that you have a copy that you have read/write access to. After that, its a simple case of cloning your fork. The recommended method is to first fork the repo, so that you have a copy that you have read/write access to. After that, its a simple case of cloning your fork.

444
Cargo.lock generated
View file

@ -135,6 +135,114 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-broadcast"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-io"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
dependencies = [
"async-lock",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "async-lock"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
"tracing",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]
name = "async-signal"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.83" version = "0.1.83"
@ -292,6 +400,19 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "blocking"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@ -358,6 +479,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.38" version = "0.4.38"
@ -419,6 +546,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -494,6 +630,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -654,6 +796,33 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "endi"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enumflags2"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.2" version = "0.1.2"
@ -692,6 +861,27 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "event-listener"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
dependencies = [
"concurrent-queue",
"parking",
"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",
"pin-project-lite",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.1" version = "2.1.1"
@ -773,6 +963,19 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.31" version = "0.3.31"
@ -1152,6 +1355,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -1753,7 +1962,6 @@ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
"data-encoding", "data-encoding",
"dns-sd",
"form_urlencoded", "form_urlencoded",
"futures-core", "futures-core",
"futures-util", "futures-util",
@ -1821,10 +2029,13 @@ dependencies = [
"librespot-core", "librespot-core",
"log", "log",
"rand", "rand",
"serde",
"serde_json", "serde_json",
"serde_repr",
"sha1", "sha1",
"thiserror", "thiserror",
"tokio", "tokio",
"zbus",
] ]
[[package]] [[package]]
@ -1931,6 +2142,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -1958,7 +2178,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.3.9",
"libc", "libc",
"wasi", "wasi",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -2014,6 +2234,19 @@ dependencies = [
"jni-sys", "jni-sys",
] ]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]] [[package]]
name = "no-std-compat" name = "no-std-compat"
version = "0.4.1" version = "0.4.1"
@ -2253,6 +2486,22 @@ dependencies = [
"paste", "paste",
] ]
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.3" version = "0.12.3"
@ -2332,6 +2581,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]] [[package]]
name = "pkcs1" name = "pkcs1"
version = "0.7.5" version = "0.7.5"
@ -2359,6 +2619,21 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "polling"
version = "3.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix",
"tracing",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.9.0" version = "1.9.0"
@ -2442,9 +2717,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "3.6.0" version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3018844a02746180074f621e847703737d27d89d7f0721a7a4da317f88b16385" checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"protobuf-support", "protobuf-support",
@ -2453,9 +2728,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf-codegen" name = "protobuf-codegen"
version = "3.6.0" version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411c15a212b4de05eb8bc989fd066a74c86bd3c04e27d6e86bd7703b806d7734" checksum = "e26b833f144769a30e04b1db0146b2aaa53fd2fd83acf10a6b5f996606c18144"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"once_cell", "once_cell",
@ -2468,9 +2743,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf-parse" name = "protobuf-parse"
version = "3.6.0" version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06f45f16b522d92336e839b5e40680095a045e36a1e7f742ba682ddc85236772" checksum = "322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indexmap", "indexmap",
@ -2484,9 +2759,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf-support" name = "protobuf-support"
version = "3.6.0" version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf96d872914fcda2b66d66ea3fff2be7c66865d31c7bb2790cff32c0e714880" checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
@ -2946,6 +3221,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_repr"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.8" version = "0.6.8"
@ -3079,6 +3365,12 @@ dependencies = [
"der", "der",
] ]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -3362,6 +3654,7 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -3495,9 +3788,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [ dependencies = [
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.32" version = "0.1.32"
@ -3539,6 +3844,17 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "uds_windows"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset",
"tempfile",
"winapi",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.17" version = "0.3.17"
@ -3598,9 +3914,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.10.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"rand", "rand",
@ -4148,6 +4464,73 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "xdg-home"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "zbus"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
dependencies = [
"async-broadcast",
"async-process",
"async-recursion",
"async-trait",
"enumflags2",
"event-listener",
"futures-core",
"futures-sink",
"futures-util",
"hex",
"nix",
"ordered-stream",
"rand",
"serde",
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.79",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [
"serde",
"static_assertions",
"zvariant",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"
@ -4174,3 +4557,40 @@ name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zvariant"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
dependencies = [
"endi",
"enumflags2",
"serde",
"static_assertions",
"zvariant_derive",
]
[[package]]
name = "zvariant_derive"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.79",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]

View file

@ -1,7 +1,7 @@
[package] [package]
name = "librespot" name = "librespot"
version = "0.5.0" version = "0.5.0"
rust-version = "1.74" rust-version = "1.75"
authors = ["Librespot Org"] authors = ["Librespot Org"]
license = "MIT" license = "MIT"
description = "An open source client library for Spotify, with support for Spotify Connect" description = "An open source client library for Spotify, with support for Spotify Connect"
@ -36,6 +36,7 @@ version = "0.5.0"
[dependencies.librespot-discovery] [dependencies.librespot-discovery]
path = "discovery" path = "discovery"
version = "0.5.0" version = "0.5.0"
default-features = false
[dependencies.librespot-metadata] [dependencies.librespot-metadata]
path = "metadata" path = "metadata"
@ -62,7 +63,7 @@ log = "0.4"
sha1 = "0.10" sha1 = "0.10"
sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } sysinfo = { version = "0.31.3", default-features = false, features = ["system"] }
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } tokio = { version = "1.40", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] }
url = "2.2" url = "2.2"
[features] [features]
@ -75,11 +76,13 @@ rodiojack-backend = ["librespot-playback/rodiojack-backend"]
sdl-backend = ["librespot-playback/sdl-backend"] sdl-backend = ["librespot-playback/sdl-backend"]
gstreamer-backend = ["librespot-playback/gstreamer-backend"] gstreamer-backend = ["librespot-playback/gstreamer-backend"]
with-dns-sd = ["librespot-core/with-dns-sd", "librespot-discovery/with-dns-sd"] with-avahi = ["librespot-discovery/with-avahi"]
with-dns-sd = ["librespot-discovery/with-dns-sd"]
with-libmdns = ["librespot-discovery/with-libmdns"]
passthrough-decoder = ["librespot-playback/passthrough-decoder"] passthrough-decoder = ["librespot-playback/passthrough-decoder"]
default = ["rodio-backend"] default = ["rodio-backend", "with-libmdns"]
[package.metadata.deb] [package.metadata.deb]
maintainer = "librespot-org" maintainer = "librespot-org"

View file

@ -29,7 +29,7 @@ RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 cross
RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf
RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.74 -y RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.75 -y
ENV PATH="/root/.cargo/bin/:${PATH}" ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add aarch64-unknown-linux-gnu
RUN rustup target add arm-unknown-linux-gnueabi RUN rustup target add arm-unknown-linux-gnueabi

View file

@ -22,7 +22,6 @@ aes = "0.8"
base64 = "0.22" base64 = "0.22"
byteorder = "1.4" byteorder = "1.4"
bytes = "1" bytes = "1"
dns-sd = { version = "0.1", optional = true }
form_urlencoded = "1.0" form_urlencoded = "1.0"
futures-core = "0.3" futures-core = "0.3"
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] }
@ -71,6 +70,3 @@ vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["macros", "parking_lot"] } tokio = { version = "1", features = ["macros", "parking_lot"] }
[features]
with-dns-sd = ["dns-sd"]

View file

@ -21,9 +21,6 @@ use url::ParseError;
use librespot_oauth::OAuthError; use librespot_oauth::OAuthError;
#[cfg(feature = "with-dns-sd")]
use dns_sd::DNSError;
#[derive(Debug)] #[derive(Debug)]
pub struct Error { pub struct Error {
pub kind: ErrorKind, pub kind: ErrorKind,
@ -314,13 +311,6 @@ impl From<DecodeError> for Error {
} }
} }
#[cfg(feature = "with-dns-sd")]
impl From<DNSError> for Error {
fn from(err: DNSError) -> Self {
Self::new(ErrorKind::Unavailable, err)
}
}
impl From<http::Error> for Error { impl From<http::Error> for Error {
fn from(err: http::Error) -> Self { fn from(err: http::Error) -> Self {
if err.is::<InvalidHeaderName>() if err.is::<InvalidHeaderName>()

View file

@ -21,13 +21,16 @@ hmac = "0.12"
hyper = { version = "1.3", features = ["http1"] } hyper = { version = "1.3", features = ["http1"] }
hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] }
http-body-util = "0.1.1" http-body-util = "0.1.1"
libmdns = "0.9" libmdns = { version = "0.9", optional = true }
log = "0.4" log = "0.4"
rand = "0.8" rand = "0.8"
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
serde_repr = "0.1"
serde_json = "1.0" serde_json = "1.0"
sha1 = "0.10" sha1 = "0.10"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } tokio = { version = "1", features = ["parking_lot", "sync", "rt"] }
zbus = { version = "4", default-features = false, features = ["tokio"], optional = true }
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"
@ -39,4 +42,8 @@ hex = "0.4"
tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } tokio = { version = "1", features = ["macros", "parking_lot", "rt"] }
[features] [features]
with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"] with-avahi = ["zbus", "serde"]
with-dns-sd = ["dns-sd"]
with-libmdns = ["libmdns"]
default = ["with-libmdns"]

151
discovery/src/avahi.rs Normal file
View file

@ -0,0 +1,151 @@
#![cfg(feature = "with-avahi")]
#[allow(unused)]
pub use server::ServerProxy;
#[allow(unused)]
pub use entry_group::{
EntryGroupProxy, EntryGroupState, StateChangedStream as EntryGroupStateChangedStream,
};
mod server {
// This is not the full interface, just the methods we need!
// Avahi also implements a newer version of the interface ("org.freedesktop.Avahi.Server2"), but
// the additions are not relevant for us, and the older version is not intended to be deprecated.
// cf. the release notes for 0.8 at https://github.com/avahi/avahi/blob/master/docs/NEWS
#[zbus::proxy(
interface = "org.freedesktop.Avahi.Server",
default_service = "org.freedesktop.Avahi",
default_path = "/",
gen_blocking = false
)]
trait Server {
/// EntryGroupNew method
#[zbus(object = "super::entry_group::EntryGroup")]
fn entry_group_new(&self);
/// GetState method
fn get_state(&self) -> zbus::Result<i32>;
/// StateChanged signal
#[zbus(signal)]
fn state_changed(&self, state: i32, error: &str) -> zbus::Result<()>;
}
}
mod entry_group {
use serde_repr::Deserialize_repr;
use zbus::zvariant;
#[derive(Clone, Copy, Debug, Deserialize_repr)]
#[repr(i32)]
pub enum EntryGroupState {
// The group has not yet been committed, the user must still call avahi_entry_group_commit()
Uncommited = 0,
// The entries of the group are currently being registered
Registering = 1,
// The entries have successfully been established
Established = 2,
// A name collision for one of the entries in the group has been detected, the entries have been withdrawn
Collision = 3,
// Some kind of failure happened, the entries have been withdrawn
Failure = 4,
}
impl zvariant::Type for EntryGroupState {
fn signature() -> zvariant::Signature<'static> {
zvariant::Signature::try_from("i").unwrap()
}
}
#[zbus::proxy(
interface = "org.freedesktop.Avahi.EntryGroup",
default_service = "org.freedesktop.Avahi",
gen_blocking = false
)]
trait EntryGroup {
/// AddAddress method
fn add_address(
&self,
interface: i32,
protocol: i32,
flags: u32,
name: &str,
address: &str,
) -> zbus::Result<()>;
/// AddRecord method
#[allow(clippy::too_many_arguments)]
fn add_record(
&self,
interface: i32,
protocol: i32,
flags: u32,
name: &str,
clazz: u16,
type_: u16,
ttl: u32,
rdata: &[u8],
) -> zbus::Result<()>;
/// AddService method
#[allow(clippy::too_many_arguments)]
fn add_service(
&self,
interface: i32,
protocol: i32,
flags: u32,
name: &str,
type_: &str,
domain: &str,
host: &str,
port: u16,
txt: &[&[u8]],
) -> zbus::Result<()>;
/// AddServiceSubtype method
#[allow(clippy::too_many_arguments)]
fn add_service_subtype(
&self,
interface: i32,
protocol: i32,
flags: u32,
name: &str,
type_: &str,
domain: &str,
subtype: &str,
) -> zbus::Result<()>;
/// Commit method
fn commit(&self) -> zbus::Result<()>;
/// Free method
fn free(&self) -> zbus::Result<()>;
/// GetState method
fn get_state(&self) -> zbus::Result<EntryGroupState>;
/// IsEmpty method
fn is_empty(&self) -> zbus::Result<bool>;
/// Reset method
fn reset(&self) -> zbus::Result<()>;
/// UpdateServiceTxt method
#[allow(clippy::too_many_arguments)]
fn update_service_txt(
&self,
interface: i32,
protocol: i32,
flags: u32,
name: &str,
type_: &str,
domain: &str,
txt: &[&[u8]],
) -> zbus::Result<()>;
/// StateChanged signal
#[zbus(signal)]
fn state_changed(&self, state: EntryGroupState, error: &str) -> zbus::Result<()>;
}
}

View file

@ -7,17 +7,19 @@
//! This library uses mDNS and DNS-SD so that other devices can find it, //! This library uses mDNS and DNS-SD so that other devices can find it,
//! and spawns an http server to answer requests of Spotify clients. //! and spawns an http server to answer requests of Spotify clients.
mod avahi;
mod server; mod server;
use std::{ use std::{
borrow::Cow, borrow::Cow,
io, error::Error as StdError,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use futures_core::Stream; use futures_core::Stream;
use thiserror::Error; use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
use self::server::DiscoveryServer; use self::server::DiscoveryServer;
@ -30,6 +32,88 @@ pub use crate::core::authentication::Credentials;
/// Determining the icon in the list of available devices. /// Determining the icon in the list of available devices.
pub use crate::core::config::DeviceType; pub use crate::core::config::DeviceType;
pub enum DiscoveryEvent {
Credentials(Credentials),
ServerError(DiscoveryError),
ZeroconfError(DiscoveryError),
}
enum ZeroconfCmd {
Shutdown,
}
pub struct DnsSdHandle {
task_handle: tokio::task::JoinHandle<()>,
shutdown_tx: oneshot::Sender<ZeroconfCmd>,
}
impl DnsSdHandle {
async fn shutdown(self) {
log::debug!("Shutting down zeroconf responder");
let Self {
task_handle,
shutdown_tx,
} = self;
if shutdown_tx.send(ZeroconfCmd::Shutdown).is_err() {
log::warn!("Zeroconf responder unexpectedly disappeared");
} else {
let _ = task_handle.await;
log::debug!("Zeroconf responder stopped");
}
}
}
pub type DnsSdServiceBuilder = fn(
Cow<'static, str>,
Vec<std::net::IpAddr>,
u16,
mpsc::UnboundedSender<DiscoveryEvent>,
) -> Result<DnsSdHandle, Error>;
// Default goes first: This matches the behaviour when feature flags were exlusive, i.e. when there
// was only `feature = "with-dns-sd"` or `not(feature = "with-dns-sd")`
pub const BACKENDS: &[(
&str,
// If None, the backend is known but wasn't compiled.
Option<DnsSdServiceBuilder>,
)] = &[
#[cfg(feature = "with-avahi")]
("avahi", Some(launch_avahi)),
#[cfg(not(feature = "with-avahi"))]
("avahi", None),
#[cfg(feature = "with-dns-sd")]
("dns-sd", Some(launch_dns_sd)),
#[cfg(not(feature = "with-dns-sd"))]
("dns-sd", None),
#[cfg(feature = "with-libmdns")]
("libmdns", Some(launch_libmdns)),
#[cfg(not(feature = "with-libmdns"))]
("libmdns", None),
];
pub fn find(name: Option<&str>) -> Result<DnsSdServiceBuilder, Error> {
if let Some(ref name) = name {
match BACKENDS.iter().find(|(id, _)| name == id) {
Some((_id, Some(launch_svc))) => Ok(*launch_svc),
Some((_id, None)) => Err(Error::unavailable(format!(
"librespot built without '{}' support",
name
))),
None => Err(Error::not_found(format!(
"unknown zeroconf backend '{}'",
name
))),
}
} else {
BACKENDS
.iter()
.find_map(|(_, launch_svc)| *launch_svc)
.ok_or(Error::unavailable(
"librespot built without zeroconf backends",
))
}
}
/// Makes this device visible to Spotify clients in the local network. /// Makes this device visible to Spotify clients in the local network.
/// ///
/// `Discovery` implements the [`Stream`] trait. Every time this device /// `Discovery` implements the [`Stream`] trait. Every time this device
@ -37,10 +121,11 @@ pub use crate::core::config::DeviceType;
pub struct Discovery { pub struct Discovery {
server: DiscoveryServer, server: DiscoveryServer,
#[cfg(not(feature = "with-dns-sd"))] /// An opaque handle to the DNS-SD service. Dropping this will unregister the service.
_svc: libmdns::Service, #[allow(unused)]
#[cfg(feature = "with-dns-sd")] svc: DnsSdHandle,
_svc: dns_sd::DNSService,
event_rx: mpsc::UnboundedReceiver<DiscoveryEvent>,
} }
/// A builder for [`Discovery`]. /// A builder for [`Discovery`].
@ -48,6 +133,7 @@ pub struct Builder {
server_config: server::Config, server_config: server::Config,
port: u16, port: u16,
zeroconf_ip: Vec<std::net::IpAddr>, zeroconf_ip: Vec<std::net::IpAddr>,
zeroconf_backend: Option<DnsSdServiceBuilder>,
} }
/// Errors that can occur while setting up a [`Discovery`] instance. /// Errors that can occur while setting up a [`Discovery`] instance.
@ -55,16 +141,27 @@ pub struct Builder {
pub enum DiscoveryError { pub enum DiscoveryError {
#[error("Creating SHA1 block cipher failed")] #[error("Creating SHA1 block cipher failed")]
AesError(#[from] aes::cipher::InvalidLength), AesError(#[from] aes::cipher::InvalidLength),
#[error("Setting up dns-sd failed: {0}")] #[error("Setting up dns-sd failed: {0}")]
DnsSdError(#[from] io::Error), DnsSdError(#[source] Box<dyn StdError + Send + Sync>),
#[error("Creating SHA1 HMAC failed for base key {0:?}")] #[error("Creating SHA1 HMAC failed for base key {0:?}")]
HmacError(Vec<u8>), HmacError(Vec<u8>),
#[error("Setting up the HTTP server failed: {0}")] #[error("Setting up the HTTP server failed: {0}")]
HttpServerError(#[from] hyper::Error), HttpServerError(#[from] hyper::Error),
#[error("Missing params for key {0}")] #[error("Missing params for key {0}")]
ParamsError(&'static str), ParamsError(&'static str),
} }
#[cfg(feature = "with-avahi")]
impl From<zbus::Error> for DiscoveryError {
fn from(error: zbus::Error) -> Self {
Self::DnsSdError(Box::new(error))
}
}
impl From<DiscoveryError> for Error { impl From<DiscoveryError> for Error {
fn from(err: DiscoveryError) -> Self { fn from(err: DiscoveryError) -> Self {
match err { match err {
@ -77,6 +174,264 @@ impl From<DiscoveryError> for Error {
} }
} }
#[allow(unused)]
const DNS_SD_SERVICE_NAME: &str = "_spotify-connect._tcp";
#[allow(unused)]
const TXT_RECORD: [&str; 2] = ["VERSION=1.0", "CPath=/"];
#[cfg(feature = "with-avahi")]
async fn avahi_task(
name: Cow<'static, str>,
port: u16,
entry_group: &mut Option<avahi::EntryGroupProxy<'_>>,
) -> Result<(), DiscoveryError> {
use self::avahi::{EntryGroupState, ServerProxy};
use futures_util::StreamExt;
let conn = zbus::Connection::system().await?;
// Wait for the daemon to show up.
// On error: Failed to listen for NameOwnerChanged signal => Fatal DBus issue
let bus = zbus::fdo::DBusProxy::new(&conn).await?;
let mut stream = bus
.receive_name_owner_changed_with_args(&[(0, "org.freedesktop.Avahi")])
.await?;
loop {
// Wait for Avahi daemon to be started
'wait_avahi: {
while let Poll::Ready(Some(_)) = futures_util::poll!(stream.next()) {
// Drain queued name owner changes, since we're going to connect in a second
}
// Ping after we connected to the signal since it might have shown up in the meantime
if let Ok(avahi_peer) =
zbus::fdo::PeerProxy::new(&conn, "org.freedesktop.Avahi", "/").await
{
if avahi_peer.ping().await.is_ok() {
log::debug!("Pinged Avahi: Available");
break 'wait_avahi;
}
}
log::warn!("Failed to connect to Avahi, zeroconf discovery will not work until avahi-daemon is started. Check that it is installed and running");
// If it didn't, wait for the signal
match stream.next().await {
Some(_signal) => {
log::debug!("Avahi appeared");
break 'wait_avahi;
}
// The stream ended, but this should never happen
None => {
return Err(zbus::Error::Failure("DBus disappeared".to_owned()).into());
}
}
}
// Connect to Avahi and publish the service
let avahi_server = ServerProxy::new(&conn).await?;
log::trace!("Connected to Avahi");
*entry_group = Some(avahi_server.entry_group_new().await?);
let mut entry_group_state_stream = entry_group
.as_mut()
.unwrap()
.receive_state_changed()
.await?;
entry_group
.as_mut()
.unwrap()
.add_service(
-1, // AVAHI_IF_UNSPEC
-1, // IPv4 and IPv6
0, // flags
&name,
DNS_SD_SERVICE_NAME, // type
"", // domain: let the server choose
"", // host: let the server choose
port,
&TXT_RECORD.map(|s| s.as_bytes()),
)
.await?;
entry_group.as_mut().unwrap().commit().await?;
log::debug!("Commited zeroconf service with name {}", &name);
'monitor_service: loop {
tokio::select! {
Some(state_changed) = entry_group_state_stream.next() => {
let (state, error) = match state_changed.args() {
Ok(sc) => (sc.state, sc.error),
Err(e) => {
log::warn!("Error on receiving EntryGroup state from Avahi: {}", e);
continue 'monitor_service;
}
};
match state {
EntryGroupState::Uncommited | EntryGroupState::Registering => {
// Not yet registered, ignore.
}
EntryGroupState::Established => {
log::info!("Published zeroconf service");
}
EntryGroupState::Collision => {
// This most likely means that librespot has unintentionally been started twice.
// Thus, don't retry with a new name, but abort.
//
// Note that the error would usually already be returned by
// entry_group.add_service above, so this state_changed handler
// won't be hit.
//
// EntryGroup has been withdrawn at this point already!
log::error!("zeroconf collision for name '{}'", &name);
return Err(zbus::Error::Failure(format!("zeroconf collision for name: {}", name)).into());
}
EntryGroupState::Failure => {
// TODO: Back off/treat as fatal?
// EntryGroup has been withdrawn at this point already!
// There seems to be no code in Avahi that actually sets this state.
log::error!("zeroconf failure: {}", error);
return Err(zbus::Error::Failure(format!("zeroconf failure: {}", error)).into());
}
}
}
_name_owner_change = stream.next() => {
break 'monitor_service;
}
}
}
// Avahi disappeared (or the service was immediately taken over by a
// new daemon) => drop all handles, and reconnect
log::info!("Avahi disappeared, trying to reconnect");
}
}
#[cfg(feature = "with-avahi")]
fn launch_avahi(
name: Cow<'static, str>,
_zeroconf_ip: Vec<std::net::IpAddr>,
port: u16,
status_tx: mpsc::UnboundedSender<DiscoveryEvent>,
) -> Result<DnsSdHandle, Error> {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let task_handle = tokio::spawn(async move {
let mut entry_group = None;
tokio::select! {
res = avahi_task(name, port, &mut entry_group) => {
if let Err(e) = res {
log::error!("Avahi error: {}", e);
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
}
},
_ = shutdown_rx => {
if let Some(entry_group) = entry_group.as_mut() {
if let Err(e) = entry_group.free().await {
log::warn!("Failed to un-publish zeroconf service: {}", e);
} else {
log::debug!("Un-published zeroconf service");
}
}
},
}
});
Ok(DnsSdHandle {
task_handle,
shutdown_tx,
})
}
#[cfg(feature = "with-dns-sd")]
fn launch_dns_sd(
name: Cow<'static, str>,
_zeroconf_ip: Vec<std::net::IpAddr>,
port: u16,
status_tx: mpsc::UnboundedSender<DiscoveryEvent>,
) -> Result<DnsSdHandle, Error> {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let task_handle = tokio::task::spawn_blocking(move || {
let inner = move || -> Result<(), DiscoveryError> {
let svc = dns_sd::DNSService::register(
Some(name.as_ref()),
DNS_SD_SERVICE_NAME,
None,
None,
port,
&TXT_RECORD,
)
.map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?;
let _ = shutdown_rx.blocking_recv();
std::mem::drop(svc);
Ok(())
};
if let Err(e) = inner() {
log::error!("dns_sd error: {}", e);
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
}
});
Ok(DnsSdHandle {
shutdown_tx,
task_handle,
})
}
#[cfg(feature = "with-libmdns")]
fn launch_libmdns(
name: Cow<'static, str>,
zeroconf_ip: Vec<std::net::IpAddr>,
port: u16,
status_tx: mpsc::UnboundedSender<DiscoveryEvent>,
) -> Result<DnsSdHandle, Error> {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let task_handle = tokio::task::spawn_blocking(move || {
let inner = move || -> Result<(), DiscoveryError> {
let svc = if !zeroconf_ip.is_empty() {
libmdns::Responder::spawn_with_ip_list(
&tokio::runtime::Handle::current(),
zeroconf_ip,
)
} else {
libmdns::Responder::spawn(&tokio::runtime::Handle::current())
}
.map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))
.unwrap()
.register(
DNS_SD_SERVICE_NAME.to_owned(),
name.into_owned(),
port,
&TXT_RECORD,
);
let _ = shutdown_rx.blocking_recv();
std::mem::drop(svc);
Ok(())
};
if let Err(e) = inner() {
log::error!("libmdns error: {}", e);
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
}
});
Ok(DnsSdHandle {
shutdown_tx,
task_handle,
})
}
impl Builder { impl Builder {
/// Starts a new builder using the provided device and client IDs. /// Starts a new builder using the provided device and client IDs.
pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Self { pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Self {
@ -90,6 +445,7 @@ impl Builder {
}, },
port: 0, port: 0,
zeroconf_ip: vec![], zeroconf_ip: vec![],
zeroconf_backend: None,
} }
} }
@ -117,6 +473,12 @@ impl Builder {
self self
} }
/// Set the zeroconf (MDNS and DNS-SD) implementation to use.
pub fn zeroconf_backend(mut self, zeroconf_backend: DnsSdServiceBuilder) -> Self {
self.zeroconf_backend = Some(zeroconf_backend);
self
}
/// Sets the port on which it should listen to incoming connections. /// Sets the port on which it should listen to incoming connections.
/// The default value `0` means any port. /// The default value `0` means any port.
pub fn port(mut self, port: u16) -> Self { pub fn port(mut self, port: u16) -> Self {
@ -129,43 +491,21 @@ impl Builder {
/// # Errors /// # Errors
/// If setting up the mdns service or creating the server fails, this function returns an error. /// If setting up the mdns service or creating the server fails, this function returns an error.
pub fn launch(self) -> Result<Discovery, Error> { pub fn launch(self) -> Result<Discovery, Error> {
let name = self.server_config.name.clone();
let zeroconf_ip = self.zeroconf_ip;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let mut port = self.port; let mut port = self.port;
let name = self.server_config.name.clone().into_owned(); let server = DiscoveryServer::new(self.server_config, &mut port, event_tx.clone())?;
let server = DiscoveryServer::new(self.server_config, &mut port)?;
let _zeroconf_ip = self.zeroconf_ip;
let svc;
#[cfg(feature = "with-dns-sd")] let launch_svc = self.zeroconf_backend.unwrap_or(find(None)?);
{ let svc = launch_svc(name, zeroconf_ip, port, event_tx)?;
svc = dns_sd::DNSService::register( Ok(Discovery {
Some(name.as_ref()), server,
"_spotify-connect._tcp", svc,
None, event_rx,
None, })
port,
&["VERSION=1.0", "CPath=/"],
)?;
}
#[cfg(not(feature = "with-dns-sd"))]
{
let _svc = if !_zeroconf_ip.is_empty() {
libmdns::Responder::spawn_with_ip_list(
&tokio::runtime::Handle::current(),
_zeroconf_ip,
)?
} else {
libmdns::Responder::spawn(&tokio::runtime::Handle::current())?
};
svc = _svc.register(
"_spotify-connect._tcp".to_owned(),
name,
port,
&["VERSION=1.0", "CPath=/"],
);
}
Ok(Discovery { server, _svc: svc })
} }
} }
@ -179,12 +519,25 @@ impl Discovery {
pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Result<Self, Error> { pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Result<Self, Error> {
Self::builder(device_id, client_id).launch() Self::builder(device_id, client_id).launch()
} }
pub async fn shutdown(self) {
tokio::join!(self.server.shutdown(), self.svc.shutdown(),);
}
} }
impl Stream for Discovery { impl Stream for Discovery {
type Item = Credentials; type Item = Credentials;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.server).poll_next(cx) match Pin::new(&mut self.event_rx).poll_recv(cx) {
// Yields credentials
Poll::Ready(Some(DiscoveryEvent::Credentials(creds))) => Poll::Ready(Some(creds)),
// Also terminate the stream on fatal server or MDNS/DNS-SD errors.
Poll::Ready(Some(
DiscoveryEvent::ServerError(_) | DiscoveryEvent::ZeroconfError(_),
)) => Poll::Ready(None),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
} }
} }

View file

@ -1,18 +1,14 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::BTreeMap, collections::BTreeMap,
convert::Infallible,
net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener}, net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener},
pin::Pin,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
task::{Context, Poll},
}; };
use aes::cipher::{KeyIvInit, StreamCipher}; use aes::cipher::{KeyIvInit, StreamCipher};
use base64::engine::general_purpose::STANDARD as BASE64; use base64::engine::general_purpose::STANDARD as BASE64;
use base64::engine::Engine as _; use base64::engine::Engine as _;
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream;
use futures_util::{FutureExt, TryFutureExt}; use futures_util::{FutureExt, TryFutureExt};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use http_body_util::{BodyExt, Full}; use http_body_util::{BodyExt, Full};
@ -24,7 +20,7 @@ use serde_json::json;
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use super::DiscoveryError; use super::{DiscoveryError, DiscoveryEvent};
use crate::{ use crate::{
core::config::DeviceType, core::config::DeviceType,
@ -47,21 +43,17 @@ struct RequestHandler {
config: Config, config: Config,
username: Mutex<Option<String>>, username: Mutex<Option<String>>,
keys: DhLocalKeys, keys: DhLocalKeys,
tx: mpsc::UnboundedSender<Credentials>, event_tx: mpsc::UnboundedSender<DiscoveryEvent>,
} }
impl RequestHandler { impl RequestHandler {
fn new(config: Config) -> (Self, mpsc::UnboundedReceiver<Credentials>) { fn new(config: Config, event_tx: mpsc::UnboundedSender<DiscoveryEvent>) -> Self {
let (tx, rx) = mpsc::unbounded_channel(); Self {
let discovery = Self {
config, config,
username: Mutex::new(None), username: Mutex::new(None),
keys: DhLocalKeys::random(&mut rand::thread_rng()), keys: DhLocalKeys::random(&mut rand::thread_rng()),
tx, event_tx,
}; }
(discovery, rx)
} }
fn active_user(&self) -> String { fn active_user(&self) -> String {
@ -202,7 +194,8 @@ impl RequestHandler {
{ {
let maybe_username = self.username.lock(); let maybe_username = self.username.lock();
self.tx.send(credentials)?; self.event_tx
.send(DiscoveryEvent::Credentials(credentials))?;
if let Ok(mut username_field) = maybe_username { if let Ok(mut username_field) = maybe_username {
*username_field = Some(String::from(username)); *username_field = Some(String::from(username));
} else { } else {
@ -258,14 +251,22 @@ impl RequestHandler {
} }
} }
pub(crate) enum DiscoveryServerCmd {
Shutdown,
}
pub struct DiscoveryServer { pub struct DiscoveryServer {
cred_rx: mpsc::UnboundedReceiver<Credentials>, close_tx: oneshot::Sender<DiscoveryServerCmd>,
_close_tx: oneshot::Sender<Infallible>, task_handle: tokio::task::JoinHandle<()>,
} }
impl DiscoveryServer { impl DiscoveryServer {
pub fn new(config: Config, port: &mut u16) -> Result<Self, Error> { pub fn new(
let (discovery, cred_rx) = RequestHandler::new(config); config: Config,
port: &mut u16,
event_tx: mpsc::UnboundedSender<DiscoveryEvent>,
) -> Result<Self, Error> {
let discovery = RequestHandler::new(config, event_tx);
let address = if cfg!(windows) { let address = if cfg!(windows) {
SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port) SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port)
} else { } else {
@ -297,7 +298,7 @@ impl DiscoveryServer {
} }
} }
tokio::spawn(async move { let task_handle = tokio::spawn(async move {
let discovery = Arc::new(discovery); let discovery = Arc::new(discovery);
let server = hyper::server::conn::http1::Builder::new(); let server = hyper::server::conn::http1::Builder::new();
@ -326,27 +327,32 @@ impl DiscoveryServer {
}); });
} }
_ = &mut close_rx => { _ = &mut close_rx => {
debug!("Shutting down discovery server");
break; break;
} }
} }
} }
graceful.shutdown().await; graceful.shutdown().await;
debug!("Discovery server stopped");
}); });
Ok(Self { Ok(Self {
cred_rx, close_tx,
_close_tx: close_tx, task_handle,
}) })
} }
}
impl Stream for DiscoveryServer { pub async fn shutdown(self) {
type Item = Credentials; let Self {
close_tx,
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Credentials>> { task_handle,
self.cred_rx.poll_recv(cx) ..
} = self;
log::debug!("Shutting down discovery server");
if close_tx.send(DiscoveryServerCmd::Shutdown).is_err() {
log::warn!("Discovery server unexpectedly disappeared");
} else {
let _ = task_handle.await;
log::debug!("Discovery server stopped");
}
} }
} }

View file

@ -22,6 +22,7 @@ use librespot::{
authentication::Credentials, cache::Cache, config::DeviceType, version, Session, authentication::Credentials, cache::Cache, config::DeviceType, version, Session,
SessionConfig, SessionConfig,
}, },
discovery::DnsSdServiceBuilder,
playback::{ playback::{
audio_backend::{self, SinkBuilder, BACKENDS}, audio_backend::{self, SinkBuilder, BACKENDS},
config::{ config::{
@ -212,11 +213,11 @@ struct Setup {
credentials: Option<Credentials>, credentials: Option<Credentials>,
enable_oauth: bool, enable_oauth: bool,
oauth_port: Option<u16>, oauth_port: Option<u16>,
enable_discovery: bool,
zeroconf_port: u16, zeroconf_port: u16,
player_event_program: Option<String>, player_event_program: Option<String>,
emit_sink_events: bool, emit_sink_events: bool,
zeroconf_ip: Vec<std::net::IpAddr>, zeroconf_ip: Vec<std::net::IpAddr>,
zeroconf_backend: Option<DnsSdServiceBuilder>,
} }
fn get_setup() -> Setup { fn get_setup() -> Setup {
@ -277,6 +278,7 @@ fn get_setup() -> Setup {
const VOLUME_RANGE: &str = "volume-range"; const VOLUME_RANGE: &str = "volume-range";
const ZEROCONF_PORT: &str = "zeroconf-port"; const ZEROCONF_PORT: &str = "zeroconf-port";
const ZEROCONF_INTERFACE: &str = "zeroconf-interface"; const ZEROCONF_INTERFACE: &str = "zeroconf-interface";
const ZEROCONF_BACKEND: &str = "zeroconf-backend";
// Mostly arbitrary. // Mostly arbitrary.
const AP_PORT_SHORT: &str = "a"; const AP_PORT_SHORT: &str = "a";
@ -327,6 +329,7 @@ fn get_setup() -> Setup {
const NORMALISATION_RELEASE_SHORT: &str = "y"; const NORMALISATION_RELEASE_SHORT: &str = "y";
const NORMALISATION_THRESHOLD_SHORT: &str = "Z"; const NORMALISATION_THRESHOLD_SHORT: &str = "Z";
const ZEROCONF_PORT_SHORT: &str = "z"; const ZEROCONF_PORT_SHORT: &str = "z";
const ZEROCONF_BACKEND_SHORT: &str = ""; // no short flag
// Options that have different descriptions // Options that have different descriptions
// depending on what backends were enabled at build time. // depending on what backends were enabled at build time.
@ -638,6 +641,12 @@ fn get_setup() -> Setup {
ZEROCONF_INTERFACE, ZEROCONF_INTERFACE,
"Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.", "Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.",
"IP" "IP"
)
.optopt(
ZEROCONF_BACKEND_SHORT,
ZEROCONF_BACKEND,
"Zeroconf (MDNS/DNS-SD) backend to use. Valid values are 'avahi', 'dns-sd' and 'libmdns', if librespot is compiled with the corresponding feature flags.",
"BACKEND"
); );
#[cfg(feature = "passthrough-decoder")] #[cfg(feature = "passthrough-decoder")]
@ -803,12 +812,22 @@ fn get_setup() -> Setup {
exit(0); exit(0);
} }
// Can't use `-> fmt::Arguments` due to https://github.com/rust-lang/rust/issues/92698
fn format_flag(long: &str, short: &str) -> String {
if short.is_empty() {
format!("`--{long}`")
} else {
format!("`--{long}` / `-{short}`")
}
}
let invalid_error_msg = let invalid_error_msg =
|long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| { |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| {
error!("Invalid `--{long}` / `-{short}`: \"{invalid}\""); let flag = format_flag(long, short);
error!("Invalid {flag}: \"{invalid}\"");
if !valid_values.is_empty() { if !valid_values.is_empty() {
println!("Valid `--{long}` / `-{short}` values: {valid_values}"); println!("Valid {flag} values: {valid_values}");
} }
if !default_value.is_empty() { if !default_value.is_empty() {
@ -1190,9 +1209,22 @@ fn get_setup() -> Setup {
} }
}; };
let enable_discovery = !opt_present(DISABLE_DISCOVERY); let no_discovery_reason = if !cfg!(any(
feature = "with-libmdns",
feature = "with-dns-sd",
feature = "with-avahi"
)) {
Some("librespot compiled without zeroconf backend".to_owned())
} else if opt_present(DISABLE_DISCOVERY) {
Some(format!(
"the `--{}` / `-{}` flag set",
DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT,
))
} else {
None
};
if credentials.is_none() && !enable_discovery && !enable_oauth { if credentials.is_none() && no_discovery_reason.is_some() && !enable_oauth {
error!("Credentials are required if discovery and oauth login are disabled."); error!("Credentials are required if discovery and oauth login are disabled.");
exit(1); exit(1);
} }
@ -1225,14 +1257,16 @@ fn get_setup() -> Setup {
Some(5588) Some(5588)
}; };
if !enable_discovery && opt_present(ZEROCONF_PORT) { if let Some(reason) = no_discovery_reason.as_deref() {
if opt_present(ZEROCONF_PORT) {
warn!( warn!(
"With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", "With {} `--{}` / `-{}` has no effect.",
DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT reason, ZEROCONF_PORT, ZEROCONF_PORT_SHORT
); );
} }
}
let zeroconf_port = if enable_discovery { let zeroconf_port = if no_discovery_reason.is_none() {
opt_str(ZEROCONF_PORT) opt_str(ZEROCONF_PORT)
.map(|port| match port.parse::<u16>() { .map(|port| match port.parse::<u16>() {
Ok(value) if value != 0 => value, Ok(value) if value != 0 => value,
@ -1268,6 +1302,16 @@ fn get_setup() -> Setup {
None => SessionConfig::default().autoplay, None => SessionConfig::default().autoplay,
}; };
if let Some(reason) = no_discovery_reason.as_deref() {
if opt_present(ZEROCONF_INTERFACE) {
warn!(
"With {} {} has no effect.",
reason,
format_flag(ZEROCONF_INTERFACE, ZEROCONF_INTERFACE_SHORT),
);
}
}
let zeroconf_ip: Vec<std::net::IpAddr> = if opt_present(ZEROCONF_INTERFACE) { let zeroconf_ip: Vec<std::net::IpAddr> = if opt_present(ZEROCONF_INTERFACE) {
if let Some(zeroconf_ip) = opt_str(ZEROCONF_INTERFACE) { if let Some(zeroconf_ip) = opt_str(ZEROCONF_INTERFACE) {
zeroconf_ip zeroconf_ip
@ -1293,6 +1337,39 @@ fn get_setup() -> Setup {
vec![] vec![]
}; };
if let Some(reason) = no_discovery_reason.as_deref() {
if opt_present(ZEROCONF_BACKEND) {
warn!(
"With {} `--{}` / `-{}` has no effect.",
reason, ZEROCONF_BACKEND, ZEROCONF_BACKEND_SHORT
);
}
}
let zeroconf_backend_name = opt_str(ZEROCONF_BACKEND);
let zeroconf_backend = no_discovery_reason.is_none().then(|| {
librespot::discovery::find(zeroconf_backend_name.as_deref()).unwrap_or_else(|_| {
let available_backends: Vec<_> = librespot::discovery::BACKENDS
.iter()
.filter_map(|(id, launch_svc)| launch_svc.map(|_| *id))
.collect();
let default_backend = librespot::discovery::BACKENDS
.iter()
.find_map(|(id, launch_svc)| launch_svc.map(|_| *id))
.unwrap_or("<none>");
invalid_error_msg(
ZEROCONF_BACKEND,
ZEROCONF_BACKEND_SHORT,
&zeroconf_backend_name.unwrap_or_default(),
&available_backends.join(", "),
default_backend,
);
exit(1);
})
});
let connect_config = { let connect_config = {
let connect_default_config = ConnectConfig::default(); let connect_default_config = ConnectConfig::default();
@ -1734,11 +1811,11 @@ fn get_setup() -> Setup {
credentials, credentials,
enable_oauth, enable_oauth,
oauth_port, oauth_port,
enable_discovery,
zeroconf_port, zeroconf_port,
player_event_program, player_event_program,
emit_sink_events, emit_sink_events,
zeroconf_ip, zeroconf_ip,
zeroconf_backend,
} }
} }
@ -1767,7 +1844,7 @@ async fn main() {
let mut sys = System::new(); let mut sys = System::new();
if setup.enable_discovery { if let Some(zeroconf_backend) = setup.zeroconf_backend {
// When started at boot as a service discovery may fail due to it // When started at boot as a service discovery may fail due to it
// trying to bind to interfaces before the network is actually up. // trying to bind to interfaces before the network is actually up.
// This could be prevented in systemd by starting the service after // This could be prevented in systemd by starting the service after
@ -1787,6 +1864,7 @@ async fn main() {
.is_group(setup.connect_config.is_group) .is_group(setup.connect_config.is_group)
.port(setup.zeroconf_port) .port(setup.zeroconf_port)
.zeroconf_ip(setup.zeroconf_ip.clone()) .zeroconf_ip(setup.zeroconf_ip.clone())
.zeroconf_backend(zeroconf_backend)
.launch() .launch()
{ {
Ok(d) => break Some(d), Ok(d) => break Some(d),
@ -1955,18 +2033,25 @@ async fn main() {
info!("Gracefully shutting down"); info!("Gracefully shutting down");
let mut shutdown_tasks = tokio::task::JoinSet::new();
// Shutdown spirc if necessary // Shutdown spirc if necessary
if let Some(spirc) = spirc { if let Some(spirc) = spirc {
if let Err(e) = spirc.shutdown() { if let Err(e) = spirc.shutdown() {
error!("error sending spirc shutdown message: {}", e); error!("error sending spirc shutdown message: {}", e);
} }
if let Some(mut spirc_task) = spirc_task { if let Some(spirc_task) = spirc_task {
shutdown_tasks.spawn(spirc_task);
}
}
if let Some(discovery) = discovery {
shutdown_tasks.spawn(discovery.shutdown());
}
tokio::select! { tokio::select! {
_ = tokio::signal::ctrl_c() => (), _ = tokio::signal::ctrl_c() => (),
_ = spirc_task.as_mut() => (), _ = shutdown_tasks.join_all() => (),
else => (),
}
}
} }
} }