feat: add openrc cargo feature for OpenRC bluetooth service management
Coding-Agent: Claude Code Model: claude-sonnet-4-5
This commit is contained in:
parent
989a56346a
commit
4ae20bc1db
6 changed files with 752 additions and 59 deletions
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
RequestConfirmation {
|
||||
device: String,
|
||||
|
|
@ -158,6 +162,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 {
|
||||
|
|
@ -524,7 +530,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) => {
|
||||
|
|
@ -668,8 +685,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());
|
||||
|
|
@ -801,8 +818,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 {
|
||||
|
|
@ -813,8 +831,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 {
|
||||
|
|
@ -873,7 +892,7 @@ fn status() -> Section<crate::pages::Message> {
|
|||
return bluetooth_service_issue(
|
||||
fl!("bluetooth", "inactive"),
|
||||
fl!("activate"),
|
||||
Message::ServiceEnable,
|
||||
Message::ServiceActivate,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1126,36 +1145,65 @@ fn multiple_adapter() -> Section<crate::pages::Message> {
|
|||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
mod systemd {
|
||||
use futures::FutureExt;
|
||||
|
||||
pub fn activate_bluetooth() -> impl Future<Output = ()> + Send {
|
||||
tokio::process::Command::new("pkexec")
|
||||
.args(["systemctl", "start", "bluetooth"])
|
||||
.status()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn enable_bluetooth() -> impl Future<Output = ()> + Send {
|
||||
tokio::process::Command::new("pkexec")
|
||||
.args(["systemctl", "enable", "--now", "bluetooth"])
|
||||
.status()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn is_bluetooth_enabled() -> bool {
|
||||
std::process::Command::new("systemctl")
|
||||
.args(["is-enabled", "bluetooth"])
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn is_bluetooth_active() -> bool {
|
||||
std::process::Command::new("systemctl")
|
||||
.args(["is-active", "bluetooth"])
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(true)
|
||||
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));
|
||||
|
||||
assert!(page.bluez_service_unknown);
|
||||
assert!(!page.service_is_enabled);
|
||||
assert!(!page.service_is_active);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
631
cosmic-settings/src/service_manager.rs
Normal file
631
cosmic-settings/src/service_manager.rs
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Service manager abstraction for managing system services.
|
||||
//!
|
||||
//! This module provides a trait-based interface for interacting with system
|
||||
//! service managers (like systemd, OpenRC, etc.), allowing for testability
|
||||
//! and flexibility across different init systems.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// Trait for managing system services.
|
||||
///
|
||||
/// Implementations are bound to a specific service name and provide
|
||||
/// methods to query service state and perform lifecycle operations.
|
||||
pub trait ServiceManager {
|
||||
/// Whether the service is configured to start on boot.
|
||||
fn is_enabled(&self) -> bool;
|
||||
|
||||
/// Whether the service is currently running.
|
||||
fn is_active(&self) -> bool;
|
||||
|
||||
/// Start the service.
|
||||
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
|
||||
/// Enable the service to start on boot and start it now.
|
||||
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
|
||||
/// Whether the service is installed on this system.
|
||||
fn is_installed(&self) -> bool;
|
||||
}
|
||||
|
||||
/// A newtype around `Box<dyn ServiceManager>` that enables `#[derive(Default)]`
|
||||
/// on containing structs. Methods on `ServiceManager` are accessible via `Deref`.
|
||||
///
|
||||
/// The `Default` impl creates a manager for the `"bluetooth"` service, which is
|
||||
/// currently the only consumer. If other services need this in the future the
|
||||
/// default can be made configurable.
|
||||
pub struct ServiceManagerHandle(Box<dyn ServiceManager>);
|
||||
|
||||
impl ServiceManagerHandle {
|
||||
pub fn new(manager: Box<dyn ServiceManager>) -> Self {
|
||||
Self(manager)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ServiceManagerHandle {
|
||||
type Target = dyn ServiceManager;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServiceManagerHandle {
|
||||
fn default() -> Self {
|
||||
Self(create_default_service_manager("bluetooth"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a warning or error after a privileged command completes.
|
||||
fn log_command_result(
|
||||
result: &Result<std::process::ExitStatus, std::io::Error>,
|
||||
description: &str,
|
||||
service: &str,
|
||||
) {
|
||||
match result {
|
||||
Ok(status) if status.success() => {}
|
||||
Ok(status) => tracing::warn!(
|
||||
"{} for '{}' failed with exit code: {:?}",
|
||||
description,
|
||||
service,
|
||||
status.code(),
|
||||
),
|
||||
Err(e) => tracing::error!("Failed to execute {} for '{}': {}", description, service, e,),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a privileged command via `pkexec`.
|
||||
///
|
||||
/// This is the common async entry point for service management actions
|
||||
/// that require elevated privileges.
|
||||
async fn run_pkexec_command(args: &[&str], description: &str, service: &str) {
|
||||
let result = tokio::process::Command::new("pkexec")
|
||||
.args(args)
|
||||
.status()
|
||||
.await;
|
||||
log_command_result(&result, description, service);
|
||||
}
|
||||
|
||||
/// Mock ServiceManager that returns configurable boolean values.
|
||||
#[cfg(test)]
|
||||
pub struct MockServiceManager {
|
||||
enabled: bool,
|
||||
active: bool,
|
||||
installed: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl MockServiceManager {
|
||||
pub fn new(enabled: bool, active: bool) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
active,
|
||||
installed: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_installed(mut self, installed: bool) -> Self {
|
||||
self.installed = installed;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl ServiceManager for MockServiceManager {
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn is_installed(&self) -> bool {
|
||||
self.installed
|
||||
}
|
||||
}
|
||||
|
||||
/// SystemD implementation of ServiceManager.
|
||||
#[cfg(feature = "systemd")]
|
||||
pub struct SystemDServiceManager {
|
||||
service_name: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "systemd")]
|
||||
impl SystemDServiceManager {
|
||||
pub fn new(service_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
service_name: service_name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "systemd")]
|
||||
impl ServiceManager for SystemDServiceManager {
|
||||
fn is_enabled(&self) -> bool {
|
||||
std::process::Command::new("systemctl")
|
||||
.args(["is-enabled", &self.service_name])
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
std::process::Command::new("systemctl")
|
||||
.args(["is-active", &self.service_name])
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let service = self.service_name.clone();
|
||||
Box::pin(async move {
|
||||
run_pkexec_command(
|
||||
&["systemctl", "start", &service],
|
||||
"systemctl start",
|
||||
&service,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let service = self.service_name.clone();
|
||||
Box::pin(async move {
|
||||
run_pkexec_command(
|
||||
&["systemctl", "enable", "--now", &service],
|
||||
"systemctl enable --now",
|
||||
&service,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn is_installed(&self) -> bool {
|
||||
std::process::Command::new("systemctl")
|
||||
.args(["cat", &self.service_name])
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// OpenRC implementation of ServiceManager.
|
||||
#[cfg(feature = "openrc")]
|
||||
pub struct OpenRcServiceManager {
|
||||
service_name: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "openrc")]
|
||||
impl OpenRcServiceManager {
|
||||
pub fn new(service_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
service_name: service_name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `rc-update show` output to check if a service is in any runlevel.
|
||||
///
|
||||
/// Uses exact service name matching to avoid false positives from prefix matches
|
||||
/// (e.g., "bluetoothd" should not match a lookup for "bluetooth").
|
||||
#[cfg(any(feature = "openrc", test))]
|
||||
fn is_service_in_runlevel_output(output: &str, service_name: &str) -> bool {
|
||||
output.lines().any(|line| {
|
||||
line.split('|')
|
||||
.next()
|
||||
.map(|name| name.trim() == service_name)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "openrc")]
|
||||
impl ServiceManager for OpenRcServiceManager {
|
||||
fn is_enabled(&self) -> bool {
|
||||
std::process::Command::new("rc-update")
|
||||
.args(["show"])
|
||||
.output()
|
||||
.map(|output| {
|
||||
is_service_in_runlevel_output(
|
||||
&String::from_utf8_lossy(&output.stdout),
|
||||
&self.service_name,
|
||||
)
|
||||
})
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
std::process::Command::new("rc-service")
|
||||
.args([&self.service_name, "status"])
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let service = self.service_name.clone();
|
||||
Box::pin(async move {
|
||||
run_pkexec_command(
|
||||
&["rc-service", &service, "start"],
|
||||
"rc-service start",
|
||||
&service,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let service = self.service_name.clone();
|
||||
Box::pin(async move {
|
||||
run_pkexec_command(
|
||||
&["rc-update", "add", &service, "default"],
|
||||
"rc-update add",
|
||||
&service,
|
||||
)
|
||||
.await;
|
||||
run_pkexec_command(
|
||||
&["rc-service", &service, "start"],
|
||||
"rc-service start",
|
||||
&service,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn is_installed(&self) -> bool {
|
||||
std::path::Path::new("/etc/init.d")
|
||||
.join(&self.service_name)
|
||||
.exists()
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the running service manager and create a manager for the named service.
|
||||
///
|
||||
/// Checks runtime directories (`/run/systemd/system`, `/run/openrc`) to determine
|
||||
/// which service manager is actively running, rather than relying on which binaries
|
||||
/// are installed. This avoids false detections when multiple service managers are
|
||||
/// installed but only one is actively managing services.
|
||||
pub fn detect_service_manager(
|
||||
service_name: impl Into<String>,
|
||||
) -> Result<Box<dyn ServiceManager>, String> {
|
||||
let service_name = service_name.into();
|
||||
|
||||
#[cfg(feature = "systemd")]
|
||||
{
|
||||
if std::path::Path::new("/run/systemd/system").exists() {
|
||||
tracing::debug!("Detected systemd service manager via /run/systemd/system");
|
||||
return Ok(Box::new(SystemDServiceManager::new(service_name)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openrc")]
|
||||
{
|
||||
if std::path::Path::new("/run/openrc").exists() {
|
||||
tracing::debug!("Detected OpenRC service manager via /run/openrc");
|
||||
return Ok(Box::new(OpenRcServiceManager::new(service_name)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut checked = Vec::new();
|
||||
#[cfg(feature = "systemd")]
|
||||
checked.push("systemd (/run/systemd/system)");
|
||||
#[cfg(feature = "openrc")]
|
||||
checked.push("openrc (/run/openrc)");
|
||||
|
||||
Err(format!(
|
||||
"Could not detect a running service manager. Checked for: {}. \
|
||||
None of these service managers appear to be running on this system.",
|
||||
checked.join(", ")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory: returns `MockServiceManager` in tests, detects the running service manager in production.
|
||||
#[cfg(test)]
|
||||
pub fn create_default_service_manager(_service_name: impl Into<String>) -> Box<dyn ServiceManager> {
|
||||
Box::new(MockServiceManager::new(false, false))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub fn create_default_service_manager(service_name: impl Into<String>) -> Box<dyn ServiceManager> {
|
||||
let service_name = service_name.into();
|
||||
detect_service_manager(&service_name).unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
"Failed to detect service manager: {}. Service management features will not be available.",
|
||||
e
|
||||
);
|
||||
tracing::warn!(
|
||||
"Using no-op service manager for '{}'. Service status will always report as enabled/active, but operations will not actually execute.",
|
||||
service_name
|
||||
);
|
||||
// Graceful degradation — app continues but service management won't work
|
||||
Box::new(NoOpServiceManager)
|
||||
})
|
||||
}
|
||||
|
||||
/// Fallback when no service manager is detected — reports everything as active but does nothing.
|
||||
struct NoOpServiceManager;
|
||||
|
||||
impl ServiceManager for NoOpServiceManager {
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn is_installed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
#[test]
|
||||
fn test_service_manager_trait_is_enabled() {
|
||||
struct TestServiceManager {
|
||||
enabled: bool,
|
||||
installed: bool,
|
||||
}
|
||||
|
||||
impl ServiceManager for TestServiceManager {
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn is_installed(&self) -> bool {
|
||||
self.installed
|
||||
}
|
||||
}
|
||||
|
||||
let test_service = TestServiceManager {
|
||||
enabled: true,
|
||||
installed: true,
|
||||
};
|
||||
|
||||
assert!(test_service.is_enabled());
|
||||
|
||||
let disabled_service = TestServiceManager {
|
||||
enabled: false,
|
||||
installed: true,
|
||||
};
|
||||
assert!(!disabled_service.is_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_manager_trait_is_active() {
|
||||
struct TestServiceManager {
|
||||
active: bool,
|
||||
installed: bool,
|
||||
}
|
||||
|
||||
impl ServiceManager for TestServiceManager {
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn is_installed(&self) -> bool {
|
||||
self.installed
|
||||
}
|
||||
}
|
||||
|
||||
let test_service = TestServiceManager {
|
||||
active: true,
|
||||
installed: true,
|
||||
};
|
||||
|
||||
assert!(test_service.is_active());
|
||||
|
||||
let inactive_service = TestServiceManager {
|
||||
active: false,
|
||||
installed: true,
|
||||
};
|
||||
assert!(!inactive_service.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_service_manager_returns_configured_values() {
|
||||
let bluetooth = MockServiceManager::new(true, true);
|
||||
assert!(bluetooth.is_enabled());
|
||||
assert!(bluetooth.is_active());
|
||||
|
||||
let disabled_bluetooth = MockServiceManager::new(false, false);
|
||||
assert!(!disabled_bluetooth.is_enabled());
|
||||
assert!(!disabled_bluetooth.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "systemd")]
|
||||
fn test_systemd_service_manager_implements_trait() {
|
||||
let bluetooth = SystemDServiceManager::new("bluetooth");
|
||||
|
||||
// Can't test actual systemd calls in unit tests, but verify the struct exists
|
||||
let _enabled: bool = bluetooth.is_enabled();
|
||||
let _active: bool = bluetooth.is_active();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_manager_trait_has_activate_method() {
|
||||
let bluetooth = MockServiceManager::new(false, false);
|
||||
bluetooth.activate().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_manager_trait_has_enable_method() {
|
||||
let bluetooth = MockServiceManager::new(false, false);
|
||||
bluetooth.enable().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_manager_activate_returns_future() {
|
||||
let bluetooth = MockServiceManager::new(false, false);
|
||||
let future = bluetooth.activate();
|
||||
future.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_manager_enable_returns_future() {
|
||||
let bluetooth = MockServiceManager::new(false, false);
|
||||
let future = bluetooth.enable();
|
||||
future.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_service_manager_uses_mock_in_tests() {
|
||||
let bluetooth = create_default_service_manager("bluetooth");
|
||||
|
||||
assert!(!bluetooth.is_enabled());
|
||||
assert!(!bluetooth.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "openrc")]
|
||||
fn test_openrc_service_manager_implements_trait() {
|
||||
let bluetooth = OpenRcServiceManager::new("bluetooth");
|
||||
|
||||
// Can't test actual OpenRC calls in unit tests, but verify the struct exists
|
||||
let _enabled: bool = bluetooth.is_enabled();
|
||||
let _active: bool = bluetooth.is_active();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_service_manager_returns_result() {
|
||||
let result = detect_service_manager("test-service");
|
||||
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
assert!(!e.is_empty());
|
||||
assert!(
|
||||
e.contains("service manager"),
|
||||
"Error message should mention service manager: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_service_manager_accepts_service_name() {
|
||||
let bluetooth = MockServiceManager::new(true, true);
|
||||
assert!(bluetooth.is_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_service_manager_is_installed_defaults_to_true() {
|
||||
let bluetooth = MockServiceManager::new(true, true);
|
||||
assert!(bluetooth.is_installed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_service_manager_with_installed_configures_state() {
|
||||
let not_installed = MockServiceManager::new(true, true).with_installed(false);
|
||||
assert!(!not_installed.is_installed());
|
||||
|
||||
let installed = MockServiceManager::new(true, true).with_installed(true);
|
||||
assert!(installed.is_installed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_op_service_manager_reports_not_installed() {
|
||||
let no_op = NoOpServiceManager;
|
||||
|
||||
// When no service manager is detected at runtime, the fallback must report
|
||||
// not-installed so DBusServiceUnknown falls through to the unknown-service path
|
||||
// instead of showing a non-functional toggle.
|
||||
assert!(!no_op.is_installed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rc_update_output_exact_service_name() {
|
||||
// Must use exact matching, not prefix matching — "bluetoothd" must not match "bluetooth".
|
||||
let output = " bluetoothd | default\n";
|
||||
let found = is_service_in_runlevel_output(output, "bluetooth");
|
||||
assert!(!found);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rc_update_output_matches_exact_name() {
|
||||
let output = " bluetooth | default\n";
|
||||
let found = is_service_in_runlevel_output(output, "bluetooth");
|
||||
assert!(found);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rc_update_output_not_found() {
|
||||
let output = " dbus | default\n network | sysinit\n";
|
||||
let found = is_service_in_runlevel_output(output, "bluetooth");
|
||||
assert!(!found);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_command_result_logs_all_branches() {
|
||||
let _guard = tracing_subscriber::fmt().with_test_writer().set_default();
|
||||
|
||||
let err_result: Result<std::process::ExitStatus, std::io::Error> = Err(
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound, "command not found"),
|
||||
);
|
||||
log_command_result(&err_result, "test command", "test-service");
|
||||
|
||||
let fail_result = std::process::Command::new("false").status();
|
||||
log_command_result(&fail_result, "test false", "test-service");
|
||||
|
||||
let ok_result = std::process::Command::new("true").status();
|
||||
log_command_result(&ok_result, "test true", "test-service");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rc_update_output_multi_line() {
|
||||
let output = " dbus | default\n bluetooth | default\n network | sysinit\n";
|
||||
let found = is_service_in_runlevel_output(output, "bluetooth");
|
||||
assert!(found);
|
||||
}
|
||||
}
|
||||
2
debian/rules
vendored
2
debian/rules
vendored
|
|
@ -9,7 +9,7 @@ override_dh_auto_clean:
|
|||
ischroot || just vendor
|
||||
|
||||
override_dh_auto_build:
|
||||
test -e vendor.tar && just build-vendored || just
|
||||
test -e vendor.tar && just build-vendored --features systemd || just build-release --features systemd
|
||||
|
||||
override_dh_auto_install:
|
||||
just rootdir=$(DESTDIR) install
|
||||
|
|
|
|||
41
justfile
41
justfile
|
|
@ -53,26 +53,31 @@ heaptrack *args:
|
|||
check-features:
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
cargo check --no-default-features
|
||||
cargo check
|
||||
for feature in \
|
||||
"page-accessibility" \
|
||||
"page-about" \
|
||||
"page-bluetooth" \
|
||||
"page-date" \
|
||||
"page-default-apps" \
|
||||
"page-display" \
|
||||
"page-input" \
|
||||
"page-legacy-applications" \
|
||||
"page-networking" \
|
||||
"page-power" \
|
||||
"page-region" \
|
||||
"page-sound" \
|
||||
"page-users" \
|
||||
"page-window-management" \
|
||||
"page-workspaces"
|
||||
for service_manager in \
|
||||
"systemd" \
|
||||
"openrc"
|
||||
do
|
||||
cargo check --no-default-features --features "${feature}"
|
||||
cargo check --no-default-features --features "${service_manager}"
|
||||
for feature in \
|
||||
"page-accessibility" \
|
||||
"page-about" \
|
||||
"page-bluetooth" \
|
||||
"page-date" \
|
||||
"page-default-apps" \
|
||||
"page-display" \
|
||||
"page-input" \
|
||||
"page-legacy-applications" \
|
||||
"page-networking" \
|
||||
"page-power" \
|
||||
"page-region" \
|
||||
"page-sound" \
|
||||
"page-users" \
|
||||
"page-window-management" \
|
||||
"page-workspaces"
|
||||
do
|
||||
cargo check --no-default-features --features "${feature},${service_manager}"
|
||||
done
|
||||
done
|
||||
|
||||
# Bump cargo version, create git commit, and create tag
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue