defmodule Systemd.DBus do
@moduledoc """
Small D-Bus client wrapper used by the systemd API.
This module intentionally keeps the surface tiny: connect to a bus and perform
method calls, returning structured results or structured errors.
"""
alias Rebus.Connection
alias Rebus.Message
alias Systemd.DBus.{Result, Signature}
alias Systemd.Error
@type bus ::
:system | :session | %{required(:family) => :local | :inet, optional(atom()) => term()}
@type call_option ::
{:destination, String.t()}
| {:path, String.t()}
| {:interface, String.t()}
| {:member, String.t()}
| {:signature, String.t()}
| {:body, [term()]}
@doc """
Connects to a D-Bus bus.
Defaults to the system bus because systemd exposes its manager API there.
"""
@spec connect(bus(), keyword()) :: {:ok, pid()} | {:error, Error.t()}
def connect(bus \\ :system, opts \\ []) do
with {:ok, _apps} <- Application.ensure_all_started(:rebus),
{:ok, conn} <- Rebus.connect(bus, opts) do
{:ok, conn}
else
{:error, %Error{} = error} -> {:error, error}
{:error, reason} -> {:error, Error.connection_error(reason)}
end
end
@doc """
Sends a method call and returns a structured D-Bus result.
"""
@spec call(pid(), [call_option()]) :: {:ok, Result.t()} | {:error, Error.t()}
def call(conn, opts) when is_pid(conn) and is_list(opts) do
with :ok <- validate_signature(opts),
{:ok, message} <- message(opts) do
send_message(conn, message)
end
end
@doc """
Sends a method call and returns only the decoded D-Bus body.
"""
@spec call_body(pid(), [call_option()]) :: {:ok, [term()]} | {:error, Error.t()}
def call_body(conn, opts) do
with {:ok, %Result{body: body}} <- call(conn, opts), do: {:ok, body}
end
defp validate_signature(opts) do
signature = Keyword.get(opts, :signature, "")
if Signature.supported?(signature) do
:ok
else
{:error, Error.encoding_error({:unsupported_signature, signature})}
end
end
defp message(opts) do
Message.new(:method_call,
destination: Keyword.fetch!(opts, :destination),
path: Keyword.fetch!(opts, :path),
interface: Keyword.fetch!(opts, :interface),
member: Keyword.fetch!(opts, :member),
signature: Keyword.get(opts, :signature, ""),
body: Keyword.get(opts, :body, [])
)
rescue
error in KeyError -> {:error, Error.validation_error(error)}
end
defp send_message(conn, message) do
conn
|> Connection.send(message)
|> to_result()
catch
:exit, {{%FunctionClauseError{}, _stacktrace}, _call} = reason ->
{:error, Error.encoding_error(reason)}
:exit, reason ->
{:error, Error.connection_error(reason)}
end
defp to_result(%Message{type: :method_return, body: body} = message) do
{:ok, %Result{message: message, body: body}}
end
defp to_result(%Message{type: :error, header_fields: header_fields, body: body}) do
{:error, Error.dbus_error(Map.get(header_fields, :error_name), body)}
end
defp to_result({:error, reason}), do: {:error, Error.connection_error(reason)}
defp to_result(other), do: {:error, Error.protocol_error(other)}
end