Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4d05958
test: add ServiceManager trait with is_enabled method
lannuttia May 18, 2026
e4a061d
feat: add is_active method to ServiceManager trait
lannuttia May 18, 2026
fdb21bc
test: add MockServiceManager with tests for ServiceManager trait
lannuttia May 18, 2026
642efc8
feat: add SystemDServiceManager implementing ServiceManager trait
lannuttia May 18, 2026
2a0bdb4
feat: add ServiceManager field to Page struct
lannuttia May 18, 2026
f7ad8a9
refactor: migrate DBusConnect to use ServiceManager trait
lannuttia May 18, 2026
62e83d8
test: add activate method to ServiceManager trait
lannuttia May 18, 2026
bb3f2c1
test: add enable method to ServiceManager trait
lannuttia May 18, 2026
e2828cf
feat: change ServiceManager::activate to return Future
lannuttia May 18, 2026
06c4314
feat: change ServiceManager::enable to return Future
lannuttia May 18, 2026
889a95f
feat: ServiceActivate uses service_manager.activate()
lannuttia May 18, 2026
1479a89
feat: ServiceEnable uses service_manager.enable()
lannuttia May 18, 2026
c03171a
feat: auto-detect service manager at runtime (Mock in tests, SystemD …
lannuttia May 18, 2026
ef8577b
refactor: remove dead code and add documentation for service manager …
lannuttia May 18, 2026
958284a
test: add service manager state verification tests
lannuttia May 18, 2026
79bd34f
refactor: extract ServiceManager trait to reusable module
lannuttia May 19, 2026
25a4aca
refactor: remove spy-based tests that couple to implementation
lannuttia May 19, 2026
03ce0ae
feat: add OpenRC service manager implementation
lannuttia May 19, 2026
032866c
refactor: add conditional compilation for service managers
lannuttia May 19, 2026
cd7fc0e
feat: add runtime service manager detection
lannuttia May 19, 2026
5ac6119
refactor: improve service manager detection robustness
lannuttia May 19, 2026
ba73b16
fix: improve error messages for service manager detection
lannuttia May 19, 2026
fe3b76e
fix: add NoOpServiceManager for production fallback
lannuttia May 19, 2026
cb4f604
refactor: bind ServiceManager to specific service name
lannuttia May 19, 2026
269dc6c
fix: use ServiceActivate when service is enabled but inactive
lannuttia May 19, 2026
32753ce
test: add coverage for MockServiceManager::with_installed and is_inst…
lannuttia May 19, 2026
6e46775
test: add coverage for DBusServiceUnknown, ServiceActivate, and Servi…
lannuttia May 19, 2026
ce51ead
refactor: remove #[allow(dead_code)] from with_installed now that it …
lannuttia May 19, 2026
c4d5b31
fix: NoOpServiceManager::is_installed should return false to show blu…
lannuttia May 19, 2026
eeaf13c
fix: OpenRC is_enabled() uses exact service name matching, not prefix…
lannuttia May 19, 2026
a5dfbca
test: gracefully skip D-Bus session tests when no session bus is avai…
lannuttia May 19, 2026
353450c
feat: add tracing diagnostics for pkexec command failures
lannuttia May 19, 2026
51d1db1
test: create in-memory mock D-Bus connection via p2p UnixStream pair
lannuttia May 19, 2026
dc7fb32
test: remove mock_dbus_connection helper and p2p-dependent tests
lannuttia May 19, 2026
b03e479
build: remove p2p feature from zbus dependency
lannuttia May 19, 2026
4f3a453
style: cargo fmt
lannuttia May 19, 2026
c4b0dda
Merge branch 'master' into fix/support-openrc-bluetooth-service-manag…
lannuttia May 19, 2026
67b835d
docs: remove 'how' comments, keep only 'why' comments
lannuttia May 19, 2026
f5a7334
refactor: introduce ServiceManagerHandle newtype to re-enable #[deriv…
lannuttia May 19, 2026
4d3ea93
refactor: move ServiceManagerHandle Default impl into service_manager.rs
lannuttia May 19, 2026
050004c
fix: check bluez_service_unknown before enabled/active in status view…
lannuttia May 19, 2026
9f5ec8d
fix: require at least one of 'systemd' or 'openrc' feature
lannuttia May 21, 2026
117e0b7
fix: explicitly pass --features systemd in debian/rules
lannuttia May 21, 2026
c5486c0
fix: add missing required service manager feature to check-features r…
lannuttia May 21, 2026
734f3e1
fix: move cargo check out of loop for check-versions recipe
lannuttia May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cosmic-settings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ git = "https://github.com/AerynOS/locales-rs"
optional = true

[features]
default = ["a11y", "linux", "single-instance", "wgpu"]
default = ["a11y", "linux", "single-instance", "wgpu", "systemd"]
gettext = ["dep:gettext-rs"]
systemd = []
openrc = []

# Default features for Linux
linux = [
Expand Down
7 changes: 7 additions & 0 deletions cosmic-settings/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
#![allow(clippy::cast_lossless)]
#![allow(clippy::too_many_lines)]

#[cfg(not(any(feature = "systemd", feature = "openrc")))]
compile_error!(
"At least one service manager feature must be enabled. \
Enable 'systemd' or 'openrc' for bluetooth service management."
);

pub mod app;
use std::str::FromStr;

Expand All @@ -16,6 +22,7 @@ pub mod config;
#[macro_use]
pub mod localize;
pub mod pages;
pub mod service_manager;
pub mod subscription;
pub mod theme;
pub mod utils;
Expand Down
112 changes: 80 additions & 32 deletions cosmic-settings/src/pages/bluetooth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ use std::sync::Arc;
use std::time::Duration;
use zbus::zvariant::OwnedObjectPath;

#[cfg(test)]
use crate::service_manager::MockServiceManager;
use crate::service_manager::ServiceManagerHandle;

enum Dialog {
// RequestAuthorization {
// device: OwnedObjectPath,
Expand Down Expand Up @@ -161,6 +165,8 @@ pub struct Page {
service_is_active: bool,

subscription: Option<tokio::sync::oneshot::Sender<()>>,

service_manager: ServiceManagerHandle,
}

impl page::Page<crate::pages::Message> for Page {
Expand Down Expand Up @@ -435,7 +441,18 @@ impl Page {
}

Event::DBusServiceUnknown => {
self.bluez_service_unknown = true;
// D-Bus activation for org.bluez is absent. On systemd this usually
// means bluez isn't installed; on OpenRC it may be installed but lack
// the D-Bus service file. Check the service manager to disambiguate.
if self.service_manager.is_installed() {
// Service manager confirms the service exists — query its real state.
self.bluez_service_unknown = false;
self.service_is_active = self.service_manager.is_active();
self.service_is_enabled = self.service_manager.is_enabled();
} else {
// Genuinely not installed — let status() show the unknown message.
self.bluez_service_unknown = true;
}
}

Event::Agent(message) => {
Expand Down Expand Up @@ -553,8 +570,8 @@ impl Page {
}

Message::DBusConnect(connection) => {
self.service_is_active = systemd::is_bluetooth_active();
self.service_is_enabled = systemd::is_bluetooth_enabled();
self.service_is_active = self.service_manager.is_active();
self.service_is_enabled = self.service_manager.is_enabled();
self.connection = Some(connection.clone());

let get_adapters_fut = get_adapters(connection.clone());
Expand Down Expand Up @@ -686,8 +703,9 @@ impl Page {
}

Message::ServiceActivate => {
return cosmic::task::future(async {
systemd::activate_bluetooth().await;
let activate_future = self.service_manager.activate();
return cosmic::task::future(async move {
activate_future.await;
tokio::time::sleep(Duration::from_secs(3)).await;

match zbus::Connection::system().await {
Expand All @@ -698,8 +716,9 @@ impl Page {
}

Message::ServiceEnable => {
return cosmic::task::future(async {
systemd::enable_bluetooth().await;
let enable_future = self.service_manager.enable();
return cosmic::task::future(async move {
enable_future.await;
tokio::time::sleep(Duration::from_secs(3)).await;

match zbus::Connection::system().await {
Expand Down Expand Up @@ -758,7 +777,7 @@ fn status() -> Section<crate::pages::Message> {
return bluetooth_service_issue(
fl!("bluetooth", "inactive"),
fl!("activate"),
Message::ServiceEnable,
Message::ServiceActivate,
);
}

Expand Down Expand Up @@ -1011,36 +1030,65 @@ fn multiple_adapter() -> Section<crate::pages::Message> {

impl page::AutoBind<crate::pages::Message> for Page {}

mod systemd {
use futures::FutureExt;
impl Page {
#[cfg(test)]
fn with_service_manager(service_manager: ServiceManagerHandle) -> Self {
Self {
service_manager,
..Page::default()
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_dbus_service_unknown_with_installed_service_queries_manager() {
let bluetooth = MockServiceManager::new(true, false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown));

assert!(page.service_is_enabled);
assert!(!page.service_is_active);
assert!(!page.bluez_service_unknown);
}

#[test]
fn test_dbus_service_unknown_with_uninstalled_service_sets_bluez_unknown() {
let bluetooth = MockServiceManager::new(true, true).with_installed(false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown));

pub fn activate_bluetooth() -> impl Future<Output = ()> + Send {
tokio::process::Command::new("pkexec")
.args(["systemctl", "start", "bluetooth"])
.status()
.map(|_| ())
assert!(page.bluez_service_unknown);
assert!(!page.service_is_enabled);
assert!(!page.service_is_active);
}

pub fn enable_bluetooth() -> impl Future<Output = ()> + Send {
tokio::process::Command::new("pkexec")
.args(["systemctl", "enable", "--now", "bluetooth"])
.status()
.map(|_| ())
#[test]
fn test_dbus_service_unknown_clears_previously_set_bluez_service_unknown() {
// Simulate a stale state and verify the handler re-checks via is_installed().
let bluetooth = MockServiceManager::new(true, true);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
page.bluez_service_unknown = true;

let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown));

assert!(!page.bluez_service_unknown);
}

pub fn is_bluetooth_enabled() -> bool {
std::process::Command::new("systemctl")
.args(["is-enabled", "bluetooth"])
.status()
.map(|status| status.success())
.unwrap_or(true)
#[tokio::test]
async fn test_service_activate_calls_through_to_service_manager() {
let bluetooth = MockServiceManager::new(false, false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::ServiceActivate);
}

pub fn is_bluetooth_active() -> bool {
std::process::Command::new("systemctl")
.args(["is-active", "bluetooth"])
.status()
.map(|status| status.success())
.unwrap_or(true)
#[tokio::test]
async fn test_service_enable_calls_through_to_service_manager() {
let bluetooth = MockServiceManager::new(false, false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::ServiceEnable);
}
}
Loading