defmodule Systemd.Error do @moduledoc """ Structured error returned by systemd and D-Bus operations. """ @type source :: :dbus | :connection | :protocol | :validation @type category :: :permission | :not_found | :timeout | :invalid | :unsupported | :unknown @type t :: %__MODULE__{ source: source(), reason: atom(), category: category(), message: String.t(), dbus_name: String.t() | nil, body: [term()], details: term() } defexception [:source, :reason, :message, :dbus_name, :category, body: [], details: nil] @doc false @spec dbus_error(String.t() | nil, [term()]) :: t() def dbus_error(name, body) when is_list(body) do %__MODULE__{ source: :dbus, reason: reason_from_dbus_name(name), category: category_from_reason(reason_from_dbus_name(name)), message: message_from_body(body, name), dbus_name: name, body: body } end @doc false @spec connection_error(term()) :: t() def connection_error(reason) do %__MODULE__{ source: :connection, reason: normalize_reason(reason), category: category_from_reason(normalize_reason(reason)), message: "D-Bus connection failed: #{inspect(reason)}", details: reason } end @doc false @spec encoding_error(term()) :: t() def encoding_error(details) do %__MODULE__{ source: :protocol, reason: :unsupported_type, category: :unsupported, message: "Unsupported D-Bus value or signature: #{inspect(details)}", details: details } end @doc false @spec protocol_error(term()) :: t() def protocol_error(details) do %__MODULE__{ source: :protocol, reason: :unexpected_reply, category: :unsupported, message: "Unexpected D-Bus reply: #{inspect(details)}", details: details } end @doc false @spec validation_error(term()) :: t() def validation_error(reason) do %__MODULE__{ source: :validation, reason: :invalid_call, category: :invalid, message: "Invalid D-Bus call: #{inspect(reason)}", details: reason } end defp message_from_body([message | _], _name) when is_binary(message), do: message defp message_from_body(_body, name) when is_binary(name), do: name defp message_from_body(_body, _name), do: "D-Bus method call failed" @doc """ Returns true when the error is a systemd/D-Bus permission or polkit denial. """ @spec permission?(t()) :: boolean() def permission?(%__MODULE__{category: :permission}), do: true def permission?(_error), do: false defp reason_from_dbus_name("org.freedesktop.DBus.Error.AccessDenied"), do: :access_denied defp reason_from_dbus_name("org.freedesktop.DBus.Error.AuthFailed"), do: :auth_failed defp reason_from_dbus_name("org.freedesktop.DBus.Error.FileNotFound"), do: :file_not_found defp reason_from_dbus_name("org.freedesktop.DBus.Error.InteractiveAuthorizationRequired"), do: :interactive_authorization_required defp reason_from_dbus_name("org.freedesktop.DBus.Error.InvalidArgs"), do: :invalid_args defp reason_from_dbus_name("org.freedesktop.DBus.Error.NoReply"), do: :no_reply defp reason_from_dbus_name("org.freedesktop.DBus.Error.ServiceUnknown"), do: :service_unknown defp reason_from_dbus_name("org.freedesktop.DBus.Error.UnknownMethod"), do: :unknown_method defp reason_from_dbus_name("org.freedesktop.DBus.Error.UnknownObject"), do: :unknown_object defp reason_from_dbus_name("org.freedesktop.systemd1.NoSuchJob"), do: :no_such_job defp reason_from_dbus_name("org.freedesktop.systemd1.NoSuchUnit"), do: :no_such_unit defp reason_from_dbus_name("org.freedesktop.systemd1.UnitExists"), do: :unit_exists defp reason_from_dbus_name(_name), do: :dbus_error defp category_from_reason(reason) when reason in [:access_denied, :auth_failed, :interactive_authorization_required], do: :permission defp category_from_reason(reason) when reason in [ :file_not_found, :no_such_job, :no_such_unit, :service_unknown, :unknown_object ], do: :not_found defp category_from_reason(reason) when reason in [:invalid_args, :invalid_call], do: :invalid defp category_from_reason(reason) when reason in [:no_reply, :timeout], do: :timeout defp category_from_reason(reason) when reason in [:unexpected_reply, :unsupported_type], do: :unsupported defp category_from_reason(_reason), do: :unknown defp normalize_reason(reason) when is_atom(reason), do: reason defp normalize_reason({reason, _}) when is_atom(reason), do: reason defp normalize_reason(_reason), do: :error end