Skip to content

feat: add OpenRC support for bluetooth service management#1982

Open
lannuttia wants to merge 45 commits into
pop-os:masterfrom
lannuttia:fix/support-openrc-bluetooth-service-management
Open

feat: add OpenRC support for bluetooth service management#1982
lannuttia wants to merge 45 commits into
pop-os:masterfrom
lannuttia:fix/support-openrc-bluetooth-service-management

Conversation

@lannuttia
Copy link
Copy Markdown
Contributor

@lannuttia lannuttia commented Apr 24, 2026

  • I have disclosed use of any AI generated code in my commit messages.
    • If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
    • In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
  • I understand these changes in full and will be able to respond to review comments.
  • My change is accurately described in the commit message.
  • My contribution is tested and working as described.
  • I have read the Developer Certificate of Origin and certify my contribution under its conditions.

Summary

Adds OpenRC init system support to cosmic-settings' bluetooth service management via a trait-based service manager abstraction, while maintaining full backwards compatibility with systemd-based distributions.
Fixes #1981

Changes

Core Implementation

  • New service manager abstraction module (src/service_manager.rs)
    • ServiceManager trait with is_enabled(), is_active(), activate(), enable(), and is_installed() methods
    • SystemDServiceManager: systemd implementation using pkexec systemctl
    • OpenRcServiceManager: OpenRC implementation using pkexec rc-service / pkexec rc-update
    • NoOpServiceManager: fallback when no supported service manager is detected at runtime (returns is_installed() = false)
    • Runtime detection via directory checks: /run/systemd/system for systemd, /run/openrc for OpenRC
    • Comprehensive test coverage (~20 test functions)
  • Enhanced bluetooth service manager (src/pages/bluetooth/mod.rs)
    • Migrated from hardcoded systemctl calls to injected ServiceManager trait
    • Proper async handling for service activation and enable operations
    • Graceful fallback via NoOpServiceManager when service manager is unavailable

Feature Flags & Build System

  • systemd support: Behind systemd feature flag (included in default features, preserves existing behavior)
  • OpenRC support: Opt-in via --features openrc flag
  • Runtime detection: When both features are compiled in, the running service manager is detected via well-known runtime directories

Documentation

  • Module-level documentation in service_manager.rs explaining the architecture

Backwards Compatibility

This change is fully backwards compatible:

  • Default build behavior unchanged (systemd feature enabled by default, detected and used automatically)
  • No changes required for existing systemd-based packages or builds
  • OpenRC support is opt-in for distributions that need it

Impact

Before

On OpenRC systems:

  • Users see "bluetooth service could not be activated" errors
  • Manual service management required (rc-service bluetooth start)
  • Status checks fail even when BlueZ is running

After

On OpenRC systems with openrc feature enabled:

  • Automatic service detection and management at runtime
  • Proper status checking (rc-service bluetooth status)
  • Enable/start commands work correctly (rc-update add / rc-service start)

For Distributors

systemd-based distributions

No changes needed — default build works as before:

cargo build --release

OpenRC-based distributions (Gentoo, Alpine, etc.)

Enable the openrc feature:

cargo build --release --features openrc

Multi-init distributions

Enable openrc for runtime detection of both init systems:

cargo build --release --features openrc

@lannuttia lannuttia marked this pull request as draft April 24, 2026 10:55
lannuttia and others added 29 commits May 19, 2026 15:20
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Refactor systemd module to support generic service queries via new
is_service_enabled and is_service_active functions. The existing
bluetooth-specific functions now delegate to these generic helpers.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Add service_manager field to Page struct to enable dependency injection
of service management implementations. This allows testing with mock
service managers while using real service managers in production.

- Add service_manager field to Page struct with Box<dyn ServiceManager>
- Convert Default derive to explicit impl with SystemDServiceManager
- Add with_service_manager constructor for tests
- Add test verifying Page can be constructed with MockServiceManager

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Update the DBusConnect message handler to query the injected service_manager
instead of calling systemd functions directly. This enables proper dependency
injection and testing with mock service managers.

- Replace systemd::is_bluetooth_active() with service_manager.is_active()
- Replace systemd::is_bluetooth_enabled() with service_manager.is_enabled()
- Add test demonstrating service manager can return different statuses

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Modified ServiceManager trait to return Pin<Box<dyn Future<Output = ()> + Send>> from activate()
- Updated MockServiceManager to return async {} wrapped in Box::pin
- Updated SystemDServiceManager to call systemd::activate_bluetooth() wrapped in Box::pin
- Added test to verify activate() returns an awaitable Future
- Updated existing tests to use async/await with activate()

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Modified ServiceManager trait to return Pin<Box<dyn Future<Output = ()> + Send>> from enable()
- Updated MockServiceManager to return async {} wrapped in Box::pin
- Updated SystemDServiceManager to call systemd::enable_bluetooth() wrapped in Box::pin
- Added test to verify enable() returns an awaitable Future
- Updated existing tests to use async/await with enable()

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Migrate the ServiceActivate message handler to use the ServiceManager
trait's activate() method instead of directly calling systemd functions.
This enables support for different service managers (SystemD, OpenRC).

Add test to verify that handling ServiceActivate calls the service
manager's activate() method.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Migrate the ServiceEnable message handler to use the ServiceManager
trait's enable() method instead of directly calling systemd functions.
This enables support for different service managers (SystemD, OpenRC).

Add test to verify that handling ServiceEnable calls the service
manager's enable() method.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…in production)

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…detection

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Test DBusConnect queries service manager for enabled/active states
- Test service manager correctly reports enabled=true, active=true
- Test service manager correctly reports enabled=true, active=false

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Move ServiceManager trait, MockServiceManager, and SystemDServiceManager to new service_manager.rs module
- Update bluetooth page to import from service_manager module
- Migrate existing service manager tests to new module

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Removed tests that use tracking/spy pattern to verify methods were called.
These tests coupled to implementation details rather than observable behavior.
Kept only behavior-focused tests that verify actual state changes.

Tests removed:
- test_page_queries_service_manager_for_bluetooth_status (redundant)
- test_service_activate_message_calls_service_manager_activate (spy)
- test_service_enable_message_calls_service_manager_enable (spy)

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Add systemd and openrc cargo features (systemd enabled by default)
- Implement OpenRcServiceManager with OpenRC command support:
  - is_enabled: checks rc-update show output
  - is_active: uses rc-service status
  - activate: uses rc-service start
  - enable: uses rc-update add + rc-service start
- Add test for OpenRC implementation (conditional on feature)
- Clean up unused imports in bluetooth tests

OpenRC implementation uses:
- rc-update show: check if service is enabled in runlevels
- rc-service status: check if service is currently running
- rc-service start: start a service
- rc-update add: enable service in default runlevel

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Add conditional compilation attributes to SystemDServiceManager (systemd feature)
- Remove embedded systemd module, make implementation standalone
- Update SystemDServiceManager to use service parameter (generic like OpenRC)
- Update create_default_service_manager() to support feature-based selection:
  1. Test mode: MockServiceManager
  2. openrc feature: OpenRcServiceManager
  3. systemd feature (default): SystemDServiceManager
- Make systemd test conditional on feature
- Remove hardcoded bluetooth service names from systemd implementation

Changes:
- SystemD activate/enable now use the service parameter instead of hardcoded 'bluetooth'
- Both implementations now follow the same pattern with async service operations
- Eliminated the systemd helper module in favor of inline implementation

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Replace compile-time feature selection with runtime detection:

Detection strategy:
1. Check for systemd: Run 'systemctl --version'
2. Check for OpenRC: Run 'rc-service --version'
3. Return descriptive error if no supported service manager found

Benefits:
- Single binary works on both systemd and OpenRC systems
- No need to compile separate versions for different init systems
- Graceful fallback with error logging if detection fails

Implementation:
- detect_service_manager(): Attempts to detect running service manager
- create_default_service_manager(): Uses detection in production, mock in tests
- Fallback to no-op MockServiceManager if detection fails (prevents crash)
- Conditional compilation still used to only include code for enabled features

Error handling:
- Descriptive error messages indicating which service managers were attempted
- Graceful degradation - app continues running but service management disabled
- Error logged via tracing for debugging

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Replace command-based detection with runtime directory checks:

SystemD detection:
- Check for /run/systemd/system directory
- This only exists when systemd is actively running as service manager
- More reliable than checking if systemctl command exists

OpenRC detection:
- Check for /run/openrc directory
- This exists when OpenRC is actively managing services
- Works correctly regardless of init system (sysvinit, runit, etc.)
- Important: OpenRC is a service manager, not an init system
  (e.g., Gentoo uses sysvinit as PID 1 but OpenRC for service management)

Benefits:
- Detects RUNNING service managers, not just installed ones
- Handles cases where multiple service managers are installed
- More accurate than checking for command existence
- No forking processes just for detection
- Added debug logging for detection results

Error messages:
- Updated to mention runtime indicators being checked
- Clearer explanation of what was attempted

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Make error messages more accurate based on enabled features:

When no features enabled:
- Clear message: "No service manager features enabled at compile time"
- Suggests enabling systemd or openrc feature

When features enabled but no service manager detected:
- Shows exactly what was checked with paths
- Example: "Checked for: systemd (/run/systemd/system), openrc (/run/openrc)"
- Clearer than previous "Attempted" wording

Benefits:
- More actionable error messages
- Shows specific runtime indicators that were checked
- Distinguishes between build-time and runtime issues
- Helps users understand why detection failed

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Problem: MockServiceManager is only available in test mode, but we were
trying to use it as a fallback in production code.

Solution: Create a separate NoOpServiceManager for production use:
- Always reports services as enabled and active
- No-op implementations for activate/enable
- Allows the application to continue running
- Service management just won't actually work

Benefits:
- Application doesn't crash if service manager detection fails
- Clear warning logged when fallback is used
- Users can still access other settings even if service management is unavailable
- Graceful degradation instead of fail-fast

Implementation:
- NoOpServiceManager: Simple struct implementing ServiceManager trait
- Used only in production when detect_service_manager() fails
- Logs warning to help with debugging

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Add service_name field to all ServiceManager implementations
- Remove service parameter from trait methods (is_enabled, is_active, activate, enable)
- Update factory functions to accept service_name parameter
- Improve variable naming: use service name instead of generic 'manager' or 'mock'
- Add warning log when falling back to NoOpServiceManager
- All tests passing with clearer, more intuitive API (e.g., bluetooth.is_enabled())

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
When the bluetooth service is enabled (will start on boot) but currently
inactive (not running), the UI button says 'activate' but was incorrectly
sending Message::ServiceEnable.

This would work for systemd (enable --now starts the service) and OpenRC
(enable runs both rc-update add and rc-service start), but it's semantically
wrong and performs unnecessary operations.

Now correctly sends Message::ServiceActivate, which only starts the service
without modifying boot configuration.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…alled

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…ceEnable handlers

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…is tested

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…ez unknown message

When no service manager is detected at runtime (e.g., building with systemd
feature on OpenRC), NoOpServiceManager is used as the fallback. Previously it
reported is_installed() = true, which caused the DBusServiceUnknown handler
to take the 'installed' branch and set service_is_enabled/active to true.
This left bluez_service_unknown = false, so the UI showed a non-functional
toggle instead of the 'bluetooth unknown' message.

By returning false, the DBusServiceUnknown handler correctly falls through
to the bluez_service_unknown = true path, displaying the appropriate message.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
@lannuttia lannuttia force-pushed the fix/support-openrc-bluetooth-service-management branch from faa321a to c4d5b31 Compare May 19, 2026 20:38
lannuttia added 12 commits May 19, 2026 17:07
… matching

The previous implementation used startswith() to match service names
against rc-update show output, which could falsely match a similar
service name (e.g. 'bluetoothd' matching a lookup for 'bluetooth').

Extracted is_service_in_runlevel_output() with exact matching (split
on '|' separator, compare trimmed name) and added dedicated tests.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…lable

Replace unwrap() with an if-let pattern that returns early when the
session bus is unavailable. This prevents test panics in headless CI
environments (e.g., GitHub Actions runners without a D-Bus session).

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Introduce log_command_result() and run_pkexec_command() helper
functions that log warnings/errors when privileged service management
commands fail, instead of silently discarding the result.

All four pkexec call sites (systemd activate/enable and OpenRC
activate/enable) now use these helpers.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Replace the early-return D-Bus skip pattern with a proper mock D-Bus
connection.  Uses zbus's p2p feature with UnixStream::pair() to create
a fully in-memory D-Bus connection (no subprocess, no filesystem,
no real session bus dependency).  Both ends of the socket-pair run
within the same process: a background task serves as the p2p server
while the test uses the client end.

The helper panics on failure — since this is an in-memory operation
with no external dependencies, any failure is a bug that should fail
the test loudly, not silently succeed.

Enabled the 'p2p' feature on the zbus dependency (additive, no
external dependencies).

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
These two tests (test_dbus_connect_queries_service_manager_when_enabled_and_active
and test_dbus_connect_handles_enabled_but_inactive_service) required creating a
zbus::Connection via UnixStream pair in p2p mode, which pulled in the uuid crate
as a transitive dependency.

The same service-manager-querying behavior is already covered by the
DBusServiceUnknown tests (test_dbus_service_unknown_with_installed_service_queries_manager
et al.), so no unique coverage is lost.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
The p2p feature is no longer needed now that the in-memory mock D-Bus
connection tests have been restructured to avoid requiring a real
zbus::Connection value. This removes the uuid crate as a transitive
dependency from production builds.

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
- Removed AAA (Arrange/Act/Assert) scaffolding from all tests
- Removed doc comments that merely restated function names
- Condensed inline comments that explained what the code does
- Preserved comments explaining rationale (why exact matching, why directory-based detection, etc.)

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…e(Default)] on Page

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
…, remove workaround assignments in handler

Coding-Agent: Claude Code
Model: claude-sonnet-4-5
@lannuttia lannuttia marked this pull request as ready for review May 19, 2026 23:38
@lannuttia
Copy link
Copy Markdown
Contributor Author

@mmstick I think this should be ready for review.

@mmstick mmstick requested a review from a team May 20, 2026 22:56
Copy link
Copy Markdown
Member

@mmstick mmstick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure debian/rules has the systemd feature added. We should also add a compiler check to ensure that one or the other is selected, but not both.

@lannuttia
Copy link
Copy Markdown
Contributor Author

I would think that we would want the ability to compile in support for both SystemD and OpenRC at the same time. It should be detecting what service manager is currently in use at runtime and using that to provide the correct implementation.

Technically speaking, I can't think of a reason that we'd need to only compile in one or the other.

On the topic of ensuring that at least one is enabled, I don't think we'd need to do that either. If neither one of the features is enabled, then you will just get the NoOpServiceManager. Then we would just lose the automated service management and will get the error about Bluez not being installed if the Bluetooth daemon isn't running.

If you think we need to make these restrictions, we can. I just want to point out that I don't think those restrictions are strictly necessary.

@mmstick
Copy link
Copy Markdown
Member

mmstick commented May 20, 2026

If both can be enabled, we should do that. Functionality that depends on it should require one or both.

lannuttia added 2 commits May 20, 2026 23:21
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
@lannuttia
Copy link
Copy Markdown
Contributor Author

The changes that you requested should be pushed now.

@lannuttia
Copy link
Copy Markdown
Contributor Author

@mmstick the issues you mentioned should be addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bluetooth settings: Hardcoded systemctl commands prevent proper integration with OpenRC

2 participants