Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
2025/02/06 Version 4.0.0-1
--------------------------
- `plusdeckctl` connects to the system bus by default
- `plusdeckctl --user` will connect to the user bus
- `plusdeckd` loads local config by default unless run as root
- systemd unit fixes
- Requires `dbus.socket`, starts after `dbus.socket`
- Wanted by `multiuser.target`
- dbus access policy
- Ownership and allowed destination for root
- Allowed destination for `plusdeck` user

2025/02/04 Version 3.0.0-3
--------------------------
- Fix install path of systemd unit
Expand Down
18 changes: 18 additions & 0 deletions dbus/org.jfhbrook.plusdeck.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <!-- -*- XML -*- -->

<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">

<busconfig>
<!-- Root user can own the plusdeck service -->
<policy user="root">
<allow own="org.jfhbrook.plusdeck"/>
<allow send_destination="org.jfhbrook.plusdeck"/>
</policy>

<!-- Allow access for the "plusdeck" group -->
<policy group="plusdeck">
<allow send_destination="org.jfhbrook.plusdeck"/>
</policy>
</busconfig>
117 changes: 102 additions & 15 deletions docs/dbus.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,18 @@ The `plusdeck` library includes a DBus service and client. This service allows f

For information on the API, visit [the API docs for `plusdeck.dbus`](./api/plusdeck.dbus.md).

## plusdeckd
## Starting the Dbus Service

The DBus service can be launched using `plusdeckd`:
`plusdeck` ships with a systemd unit that configures the service as a Dbus service. To set up the service, run:

```sh
$ plusdeckd --help
Usage: plusdeckd [OPTIONS]

Expose the Plus Deck 2C PC Cassette Deck as a DBus service.

Options:
-C, --config-file PATH A path to a config file
--log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Set the log level
--help Show this message and exit.
sudo systemctl enable plusdeck
sudo systemctl start plusdeck # optional
```

In most cases, this can be called without arguments. By default, `plusdeckd` will use the global config file at `/etc/plusdeck.yml`.
This unit will start on the `system` bus, under the root user.

## plusdeckctl
## Running `plusdeckctl`

Assuming `plusdeckd` is running, you may interact with the service using `plusdeckctl`:

Expand All @@ -37,6 +29,7 @@ Options:
--log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Set the log level
--output [text|json] Output either human-friendly text or JSON
--user / --no-user Connect to the user bus
--help Show this message and exit.

Commands:
Expand All @@ -47,8 +40,102 @@ Commands:
pause Pause the tape
play Play a tape
rewind Rewind a tape
state Get the current state
stop Stop the tape
subscribe Subscribe to state changes
```

The interface is *very* similar to the vanilla `plusdeck` CLI. Note, however, that the config commands are slightly different. `plusdeckd` doesn't watch or reload the configuration in-place, so `plusdeckctl` will instead show the drift between the relevant config file and the loaded configuration. To synchronize the configuration, restart `plusdeckd` - if running under systemd, this will be `systemctl restart plusdeck` or similar.
The interface is similar to the vanilla `plusdeck` CLI. However, there are a few differences:

1. By default, `plusdeckctl` will connect to the `system` bus. To connect to the local bus, set the `--user` flag.
2. Configuration commands do not reload `plusdeckctl`'s configuration. Instead, they will update the relevant config file, and show the differences between the file config and the service's loaded config.
3. If the config file isn't owned by the user, `plusdeckctl` will attempt to run editing commands with `sudo`.

## Dbus Access Policies

**NOTE: Full access for `plusdeck` group access is an area of active development. This feature does not work - at least, on Fedora.** To follow along, view [this StackExchange post](https://unix.stackexchange.com/questions/790750/dbus-policy-that-allows-group-to-access-system-service). and this [Fedora discussion post](https://discussion.fedoraproject.org/t/dbus-policy-that-allows-group-to-access-system-service/144265).

When running services under the `system` bus, care must be taken to manage access policies. Dbus does this primarily with [an XML-based policy language](https://dbus.freedesktop.org/doc/dbus-daemon.1.html). Systemd additionally manages access to privileged methods, seemingly with the intent of delegating to polkit.

By default, Dbus is configured with the following policies:

* The root user may own the bus, and send and receive messages from `org.jfhbrook.plusdeck`
* Users in the `plusdeck` Unix group may additionally send and receive messages from `org.jfhbrook.plusdeck`

This means that, if the service is running, `sudo plusdeckctl` commands should always work; and that if your user is in the `plusdeck` Unix group, Dbus will allow for unprivileged `plusdeckctl` commands as well. You can create this group and add yourself to it by running:

```bash
sudo groupadd plusdeck
sudo usermod -a -G plusdeck "${USER}"
```

### Polkit

**NOTE: The Polkit policies have not been shown to work at this time.**

Prototype Polkit policies/rules may be found in the `./polkit` folder.

## Running `plusdeckd` Directly

The DBus service can be launched directly using `plusdeckd`:

```sh
$ plusdeckd --help
Usage: plusdeckd [OPTIONS]

Expose the Plus Deck 2C PC Cassette Deck as a DBus service.

Options:
-C, --config-file PATH A path to a config file
--log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Set the log level
--help Show this message and exit.
```

In most cases, this can be called without arguments. By default, `plusdeckd` will listen on the `system` bus and load the global config file (`/etc/plusdeck.yml`) if launched as root; and otherwise listen on the `user` bus and load the user's config file (`~/.config/plusdeck.yml`).

## Debugging Dbus

### Default Dbus Configuration

The default Dbus configuration is at `/usr/share/dbus-1/system.conf`. It may be useful to refer to this file when trying to understand what default access policies are being applied.

### Monitoring Dbus

The best tool for debugging Dbus seems to be [dbus-monitor](https://dbus.freedesktop.org/doc/dbus-monitor.1.html). To follow system bus messages, run:

```sh
sudo dbus-monitor --system
```

### Viewing Dbus Logs

You can review recent logs by checking the status of the `dbus` unit:

```sh
sudo systemctl status dbus
```

### Viewing the Dbus Interface

I have a just task for that:

```sh
just get-dbus-iface
```

### Debugging SELinux

While I haven't seen this to be the case, it seems theoretically possible for SELinux to block access to Dbus.

You should be able to see access denials due to SELinux by running either:

```sh
sudo ausearch -ts recent
```

or:

```sh
sudo tail -f /var/log/audit/audit.log
```
25 changes: 21 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,27 @@ tox:
clean-tox:
rm -rf .tox

# Install systemd service files and dbus config for development purposes
install-service:
sudo install -p -D -m 0644 systemd/plusdeck.service /usr/lib/systemd/system/plusdeck.service
sudo install -p -D -m 0644 dbus/org.jfhbrook.plusdeck.conf /usr/share/dbus-1/system.d/org.jfhbrook.plusdeck.conf

install-polkit-config:
sudo install -p -D -m 0644 polkit/org.jfhbrook.plusdeck.policy /usr/share/polkit-1/actions/org.jfhbrook.plusdeck.policy
sudo install -p -D -m 0644 polkit/org.jfhbrook.plusdeck.rules /usr/share/polkit-1/rules.d/org.jfhbrook.plusdeck.rules

remove-polkit-config:
sudo rm -f /usr/share/polkit-1/actions/org.jfhbrook.plusdeck.policy
sudo rm -f /usr/share/polkit-1/rules.d/org.jfhbrook.plusdeck.rules

# Pull the plusdeck service's logs with journalctl
service-logs:
journalctl -xeu plusdeck.service

# Fetch the dbus interface for the live service from dbus
get-dbus-iface:
./scripts/get-dbus-iface.sh

#
# Shell and console
#
Expand All @@ -130,10 +151,6 @@ build-docs:
# Package publishing
#

#
# Package publishing
#

# Build the package
build:
uv build
Expand Down
8 changes: 5 additions & 3 deletions plusdeck.spec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Name: plusdeck
Version: 3.0.0
Release: 3
Version: 4.0.0
Release: 1
License: MPL-2.0
Summary: Serial client and Linux service for Plus Deck 2C PC Cassette Deck

Expand All @@ -9,6 +9,7 @@ Source0: %{name}-%{version}.tar.gz
BuildArch: noarch

Requires: python-plusdeck
Requires: python-sdbus

%description

Expand All @@ -24,13 +25,14 @@ tar -xzf %{SOURCE0}
%install
mkdir -p %{buildroot}%{_prefix}/lib/systemd/system
install -p -D -m 0644 systemd/plusdeck.service %{buildroot}%{_prefix}/lib/systemd/system/plusdeck.service

install -p -D -m 0644 dbus/org.jfhbrook.plusdeck.conf %{buildroot}%{_prefix}/share/dbus-1/system.d/org.jfhbrook.plusdeck.conf

%check


%files
%{_prefix}/lib/systemd/system/plusdeck.service
%{_prefix}/share/dbus-1/system.d/org.jfhbrook.plusdeck.conf

%changelog
* Thu Feb 06 2025 Josh Holbrook <josh.holbrook@gmail.com> 3.0.0-3
Expand Down
4 changes: 3 additions & 1 deletion plusdeck.spec.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Source0: %{name}-%{version}.tar.gz
BuildArch: noarch

Requires: python-plusdeck
Requires: python-sdbus

%description

Expand All @@ -24,13 +25,14 @@ tar -xzf %{SOURCE0}
%install
mkdir -p %{buildroot}%{_prefix}/lib/systemd/system
install -p -D -m 0644 systemd/plusdeck.service %{buildroot}%{_prefix}/lib/systemd/system/plusdeck.service

install -p -D -m 0644 dbus/org.jfhbrook.plusdeck.conf %{buildroot}%{_prefix}/share/dbus-1/system.d/org.jfhbrook.plusdeck.conf

%check


%files
%{_prefix}/lib/systemd/system/plusdeck.service
%{_prefix}/share/dbus-1/system.d/org.jfhbrook.plusdeck.conf

%changelog
{{ .Env.CHANGELOG }}
20 changes: 12 additions & 8 deletions plusdeck/dbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from unittest.mock import Mock

import click
from sdbus import sd_bus_open_system, sd_bus_open_user, SdBus

from plusdeck.cli import async_command, AsyncCommand, echo, LogLevel, OutputMode, STATE
from plusdeck.client import State
Expand All @@ -27,12 +28,12 @@ class DbusClient(DbusInterface):
A DBus client for the Plus Deck 2C PC Cassette Deck.
"""

def __init__(self: Self) -> None:
def __init__(self: Self, bus: Optional[SdBus] = None) -> None:
client = Mock(name="client", side_effect=NotImplementedError("client"))
self.subscribe = Mock(name="client.subscribe")
super().__init__("", client)
super().__init__(client)

cast(Any, self)._proxify(DBUS_NAME, "/")
cast(Any, self)._proxify(DBUS_NAME, "/", bus=bus)

async def staged_config(self: Self) -> StagedConfig:
"""
Expand All @@ -54,6 +55,7 @@ class Obj:
client: DbusClient
log_level: LogLevel
output: OutputMode
user: bool


def pass_config(fn: AsyncCommand) -> AsyncCommand:
Expand Down Expand Up @@ -146,11 +148,12 @@ def warn_dirty() -> None:
default="text",
help="Output either human-friendly text or JSON",
)
@click.option(
"--user/--no-user", type=bool, default=False, help="Connect to the user bus"
)
@click.pass_context
def main(
ctx: click.Context,
log_level: LogLevel,
output: OutputMode,
ctx: click.Context, log_level: LogLevel, output: OutputMode, user: bool
) -> None:
"""
Control your Plus Deck 2C Cassette Drive through dbus.
Expand All @@ -162,8 +165,9 @@ def main(
echo.mode = output

async def load() -> None:
client = DbusClient()
ctx.obj = Obj(client=client, log_level=log_level, output=output)
bus: SdBus = sd_bus_open_user() if user else sd_bus_open_system()
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I figured out that the default bus is the system bus when started with a non-user systemd unit: https://www.freedesktop.org/software/systemd/man/latest/sd_bus_default.html based on me being able to get past a "where is the bus tho" error, I seem to understand this correctly.

client = DbusClient(bus)
ctx.obj = Obj(client=client, log_level=log_level, output=output, user=user)

asyncio.run(load())

Expand Down
4 changes: 2 additions & 2 deletions plusdeck/dbus/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
DBUS_NAME = "org.jfhbrook.plusdeck"


async def load_client(config_file: str) -> Client:
async def load_client(config_file: Optional[str]) -> Client:
config: Config = Config.from_file(config_file)

client = await create_connection(config.port)
Expand All @@ -33,7 +33,7 @@ class DbusInterface( # type: ignore
A DBus interface for controlling the Plus Deck 2C PC Cassette Deck.
"""

def __init__(self: Self, config_file: str, client: Client) -> None:
def __init__(self: Self, client: Client, config_file: Optional[str] = None) -> None:
super().__init__()
self._config: Config = Config.from_file(config_file)
self.client: Client = client
Expand Down
Loading