diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa83a58..c487432 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/dbus/org.jfhbrook.plusdeck.conf b/dbus/org.jfhbrook.plusdeck.conf
new file mode 100644
index 0000000..4eb51d5
--- /dev/null
+++ b/dbus/org.jfhbrook.plusdeck.conf
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/dbus.md b/docs/dbus.md
index c18277b..ade95ca 100644
--- a/docs/dbus.md
+++ b/docs/dbus.md
@@ -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`:
@@ -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:
@@ -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
+```
diff --git a/justfile b/justfile
index d560cec..6657675 100644
--- a/justfile
+++ b/justfile
@@ -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
#
@@ -130,10 +151,6 @@ build-docs:
# Package publishing
#
-#
-# Package publishing
-#
-
# Build the package
build:
uv build
diff --git a/plusdeck.spec b/plusdeck.spec
index 1154411..d7c1c88 100644
--- a/plusdeck.spec
+++ b/plusdeck.spec
@@ -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
@@ -9,6 +9,7 @@ Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
Requires: python-plusdeck
+Requires: python-sdbus
%description
@@ -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 3.0.0-3
diff --git a/plusdeck.spec.tmpl b/plusdeck.spec.tmpl
index 75c9bf2..0ccbdfb 100644
--- a/plusdeck.spec.tmpl
+++ b/plusdeck.spec.tmpl
@@ -9,6 +9,7 @@ Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
Requires: python-plusdeck
+Requires: python-sdbus
%description
@@ -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 }}
diff --git a/plusdeck/dbus/client.py b/plusdeck/dbus/client.py
index 4ffe02a..a199e4b 100644
--- a/plusdeck/dbus/client.py
+++ b/plusdeck/dbus/client.py
@@ -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
@@ -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:
"""
@@ -54,6 +55,7 @@ class Obj:
client: DbusClient
log_level: LogLevel
output: OutputMode
+ user: bool
def pass_config(fn: AsyncCommand) -> AsyncCommand:
@@ -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.
@@ -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()
+ client = DbusClient(bus)
+ ctx.obj = Obj(client=client, log_level=log_level, output=output, user=user)
asyncio.run(load())
diff --git a/plusdeck/dbus/interface.py b/plusdeck/dbus/interface.py
index f541faf..bacc607 100644
--- a/plusdeck/dbus/interface.py
+++ b/plusdeck/dbus/interface.py
@@ -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)
@@ -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
diff --git a/plusdeck/dbus/service.py b/plusdeck/dbus/service.py
index 8dc917b..8a5dcc2 100644
--- a/plusdeck/dbus/service.py
+++ b/plusdeck/dbus/service.py
@@ -1,5 +1,7 @@
import asyncio
import logging
+import os
+from typing import Optional
import click
from sdbus import ( # pyright: ignore [reportMissingModuleSource]
@@ -13,13 +15,13 @@
logger = logging.getLogger(__name__)
-async def service(config_file: str) -> DbusInterface:
+async def service(config_file: Optional[str] = None) -> DbusInterface:
"""
Create a configure DBus service with a supplied config file.
"""
client = await load_client(config_file)
- iface = DbusInterface(config_file, client)
+ iface = DbusInterface(client, config_file=config_file)
logger.debug(f"Requesting bus name {DBUS_NAME}...")
await request_default_bus_name_async(DBUS_NAME)
@@ -33,7 +35,7 @@ async def service(config_file: str) -> DbusInterface:
return iface
-async def serve(config_file: str) -> None:
+async def serve(config_file: Optional[str] = None) -> None:
"""
Create and serve configure DBus service with a supplied config file.
"""
@@ -44,6 +46,13 @@ async def serve(config_file: str) -> None:
@click.command
+@click.option(
+ "--global/--no-global",
+ "global_",
+ default=os.geteuid() == 0,
+ help=f"Load the global config file at {GLOBAL_FILE} "
+ "(default true when called with sudo)",
+)
@click.option(
"--config-file",
"-C",
@@ -58,14 +67,24 @@ async def serve(config_file: str) -> None:
default="INFO",
help="Set the log level",
)
-def main(config_file: str, log_level: LogLevel) -> None:
+def main(global_: bool, config_file: str, log_level: LogLevel) -> None:
"""
Expose the Plus Deck 2C PC Cassette Deck as a DBus service.
"""
logging.basicConfig(level=getattr(logging, log_level))
- asyncio.run(serve(config_file))
+ file = None
+ if config_file:
+ if global_:
+ logger.debug(
+ "--config-file is specified, so --global flag will be ignored."
+ )
+ file = config_file
+ elif global_:
+ file = GLOBAL_FILE
+
+ asyncio.run(serve(file))
if __name__ == "__main__":
diff --git a/polkit/org.jfhbrook.plusdeck.policy b/polkit/org.jfhbrook.plusdeck.policy
new file mode 100644
index 0000000..75bf3df
--- /dev/null
+++ b/polkit/org.jfhbrook.plusdeck.policy
@@ -0,0 +1,14 @@
+
+
+
+ plusdeck
+ https://github.com/jfhbrook/plusdeck
+
+ Polkit no allow eject tho
+
+ yes
+ yes
+ yes
+
+
+
diff --git a/polkit/org.jfhbrook.plusdeck.rules b/polkit/org.jfhbrook.plusdeck.rules
new file mode 100644
index 0000000..13865f4
--- /dev/null
+++ b/polkit/org.jfhbrook.plusdeck.rules
@@ -0,0 +1,14 @@
+polkit.addRule(function(action, subject) {
+ if ((action.id == "org.jfhbrook.plusdeck.Eject" ||
+ action.id == "org.jfhbrook.plusdeck.FastForwardA" ||
+ action.id == "org.jfhbrook.plusdeck.Pause" ||
+ action.id == "org.jfhbrook.plusdeck.PlayA" ||
+ action.id == "org.jfhbrook.plusdeck.PlayB" ||
+ action.id == "org.jfhbrook.plusdeck.Stop" ||
+ action.id == "org.jfhbrook.plusdeck.WaitFor") &&
+ subject.isInGroup("plusdeck")) {
+ return polkit.Result.YES;
+ }
+
+ return polkit.Result.NOT_HANDLED;
+});
diff --git a/pyproject.toml b/pyproject.toml
index 9a1c358..b7fc945 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,11 +5,11 @@ build-backend = "setuptools.build_meta"
[tool.rpm]
# Generally this is "1", but can be incremented to roll up bugfixes in the
# top-level `plusdeck` Fedora package.
-release = "3"
+release = "1"
[project]
name = "plusdeck"
-version = "3.0.0"
+version = "4.0.0"
authors = [
{name = "Josh Holbrook", email = "josh.holbrook@gmail.com"}
]
diff --git a/scripts/get-dbus-iface.sh b/scripts/get-dbus-iface.sh
new file mode 100755
index 0000000..0af109f
--- /dev/null
+++ b/scripts/get-dbus-iface.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+function extract-response {
+ tail -n +2 | sed 's/^\s*string "//' | sed 's/"$//'
+}
+
+dbus-send --system \
+ --dest=org.jfhbrook.plusdeck "/" \
+ --print-reply \
+ org.freedesktop.DBus.Introspectable.Introspect \
+ | extract-response
diff --git a/scripts/release-version.py b/scripts/release-version.py
index 82568b4..91e1127 100755
--- a/scripts/release-version.py
+++ b/scripts/release-version.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python
-import toml
+import tomllib
-with open("pyproject.toml", "r") as f:
- project = toml.load(f)
+with open("pyproject.toml", "rb") as f:
+ project = tomllib.load(f)
print(project["tool"]["rpm"]["release"])
diff --git a/scripts/version.py b/scripts/version.py
index 218040c..dba81e8 100755
--- a/scripts/version.py
+++ b/scripts/version.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python
-import toml
+import tomllib
-with open("pyproject.toml", "r") as f:
- project = toml.load(f)
+with open("pyproject.toml", "rb") as f:
+ project = tomllib.load(f)
print(project["project"]["version"])
diff --git a/systemd/plusdeck.service b/systemd/plusdeck.service
index 7d53be5..b67268c 100644
--- a/systemd/plusdeck.service
+++ b/systemd/plusdeck.service
@@ -1,5 +1,7 @@
[Unit]
Description=Plus Deck 2C PC Cassette Deck
+Requires=dbus.socket
+After=dbus.socket
[Service]
Type=dbus
@@ -7,3 +9,5 @@ BusName=org.jfhbrook.plusdeck
ExecStart=/usr/bin/plusdeckd
Restart=on-failure
+[Install]
+WantedBy=multi-user.target
diff --git a/uv.lock b/uv.lock
index 32233ed..c4601c7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1415,7 +1415,7 @@ wheels = [
[[package]]
name = "plusdeck"
-version = "3.0.0"
+version = "4.0.0"
source = { editable = "." }
dependencies = [
{ name = "click" },