Skip to main content

lib/systemd/dbus.ex

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