Rebus (rebus v0.3.0)
View SourceA D-Bus client for Elixir.
Rebus connects to system and session buses, Unix sockets, and TCP endpoints. Use it to call methods, send messages, subscribe to signals, and pass Unix file descriptors on supported local connections.
Example
# Connect to the session bus
{:ok, conn} = Rebus.connect(:session)
message = Rebus.Message.new!(:method_call,
path: "/org/freedesktop/DBus",
interface: "org.freedesktop.DBus",
destination: "org.freedesktop.DBus",
member: "ListNames"
)
{:ok, %Rebus.Message{type: :method_return, body: [names]}} = Rebus.call(conn, message)
:ok = Rebus.close(conn)Supported platforms
Rebus supports Linux and macOS, which are the platforms exercised in CI. Other Unix variants are untested; Unix file descriptor passing in particular is limited to Linux and macOS. Windows is not supported.
Configuration and guides
You can configure the system bus address in your application's config:
config :rebus, :system_bus_address, "unix:path=/run/dbus/system_bus_socket"The DBUS_SYSTEM_BUS_ADDRESS environment variable, when set and non-empty,
overrides this config key.
:match_recovery_max_rules caps how many distinct match rules a single
connection may hold whose bus-side state is uncertain. The default is 64, and
a connection that reaches the cap is closed. See
Signal subscriptions and match rules.
config :rebus, :match_recovery_max_rules, 64:match_recovery_max_attempts caps how many times the cleanup of one such
rule is retried before the connection is closed. The default is 30, about 26
seconds of backoff. See
Signal subscriptions and match rules.
config :rebus, :match_recovery_max_attempts, 30Connections implement
org.freedesktop.DBus.Peer (Ping and GetMachineId) and reply to every
other method call with an org.freedesktop.DBus.Error.UnknownMethod error,
unless the call is marked :no_reply_expected. There is no service-side API
for application methods.
See Authentication, Signal subscriptions and match rules, and Unix file descriptor passing for practical guidance.
Summary
Types
Failure reasons returned by call/3.
Failure reasons returned by add_match/3 and remove_match/3.
Functions
Subscribes the calling process to signals selected by a match rule.
Registers the calling process to receive every signal on the connection.
Sends a method call and waits for its correlated reply.
Stops a local connection process created by connect/2.
Connects to a D-Bus endpoint and returns its connection process.
Same as connect/2, but raises ArgumentError on failure.
Stops a signal handler registered by add_signal_handler/1.
Removes a match-rule subscription reference.
Sends a message without waiting for a reply.
Sends a message with a custom dispatch timeout in milliseconds.
Types
@type address() :: :system | :session | :socket.sockaddr_in() | :socket.sockaddr_in6() | :socket.sockaddr_un()
@type call_error() :: Rebus.Message.t() | error_reason()
Failure reasons returned by call/3.
A D-Bus error reply from the peer is a definitive answer rather than a
transport failure, so it is returned as the complete
%Rebus.Message{type: :error}. Everything else is a local or transport
failure reason.
@type error_reason() :: :timeout | {:reply_dropped, :method_return | {:error, binary()}} | :not_connected | :encode_failed | :disconnected | :reply_expected | :no_reply_expected | :serial_exhausted | :remote_connection_unsupported | :unix_fd_not_negotiated | :unix_fd_unsupported | :unix_fd_send_failed | :fd_claim_expired | {:invalid_message_type, Rebus.Message.message_type()}
Failure reasons returned by call/3, send/2 and send/3.
Each function documents the subset it can return.
@type match_error_reason() :: :timeout | :not_connected | :disconnected | :remote_connection_unsupported | :encode_failed | :serial_exhausted | :fd_claim_expired | :invalid_bus_reply | :match_rule_cleanup_pending | :match_subscription_state_lost | :sender_routing_ambiguous | :not_started | :not_a_bus | {:reply_dropped, :method_return | {:error, binary()}} | {:bus_error, binary()}
Failure reasons returned by add_match/3 and remove_match/3.
See Signal subscriptions and match rules for what each one means.
Functions
@spec add_match(pid(), Rebus.MatchRule.t(), non_neg_integer()) :: {:ok, reference()} | {:error, match_error_reason()}
Subscribes the calling process to signals selected by a match rule.
Signals matching the rule arrive as {ref, %Rebus.Message{}}. Build the rule
with Rebus.MatchRule.new/1; raw strings are not accepted. Equivalent rules
share one bus registration, and each call gets its own reference.
{:error, :timeout} is ambiguous: you get no reference, but the bus may
already hold the rule. A reply whose header fields exceed a local decoding
limit has the same result because its reply serial cannot be trusted; see
call/3 for the fuller contract. {:error, :sender_routing_ambiguous} means the rule
overlaps an existing one with a different sender. {:error, :not_a_bus}
means the connection was opened with bus: false, and nothing was sent.
Rebus closes the connection when too many ambiguous cleanups accumulate, or when
one remains unresolved after its retry budget. It also closes the connection
after {:error, :match_subscription_state_lost}, because closing makes the bus
discard the connection's rules.
Return values
Success is {:ok, reference}. Every failure is {:error, reason}: :timeout, :not_a_bus,
:sender_routing_ambiguous, :match_rule_cleanup_pending, :match_subscription_state_lost,
{:bus_error, error_name}, :invalid_bus_reply, :not_connected, :disconnected,
:encode_failed, :serial_exhausted, :fd_claim_expired, :not_started,
{:reply_dropped, outcome} and :remote_connection_unsupported. The
match rules guide lists what each one means.
Example
rule = Rebus.MatchRule.new!(interface: "org.example.Status", member: "Changed")
{:ok, ref} = Rebus.add_match(conn, rule)
receive do
{^ref, %Rebus.Message{type: :signal}} -> :ok
end
@spec add_signal_handler(pid()) :: {:ok, reference()} | {:error, :not_connected | :timeout | :disconnected}
Registers the calling process to receive every signal on the connection.
This asks the bus for nothing; use add_match/3 to have the bus route more
signals here. A connection can carry several handlers, each receiving every
signal that arrives on it. Rebus removes a handler when its process exits or
the connection closes. Call delete_signal_handler/2 as soon as you stop
wanting signals, so a busy bus does not fill the process mailbox.
Return values
{:ok, ref}- the handler is registered.{:error, :not_connected}- connection setup has not completed.{:error, :timeout}- the connection did not service the request in time.{:error, :disconnected}- the connection has stopped.
Examples
{:ok, ref} = Rebus.add_signal_handler(conn)Each signal then arrives in the calling process as:
{^ref, %Rebus.Message{
type: :signal,
header_fields: %{path: path, interface: interface, member: member, sender: sender},
body: [signal_args]
}}
@spec call(pid(), Rebus.Message.t(), non_neg_integer()) :: {:ok, Rebus.Message.t()} | {:error, call_error()}
Sends a method call and waits for its correlated reply.
Only a method call that expects a reply is accepted, and timeout is in
milliseconds. Each result carries the complete message, including any received
descriptors in :unix_fds. A D-Bus error reply from the peer is returned as
{:error, %Rebus.Message{type: :error}}.
Return values
{:ok, %Rebus.Message{type: :method_return}}- the peer replied successfully.{:error, %Rebus.Message{type: :error}}- the peer returned a D-Bus error reply.{:error, :timeout}- no reply arrived in time, and the request may already have reached the peer. A request sent to a named PID whoseconnect/2has not yet returned was never written and is safe to retry. A reply that exhausted a local decoding cap inside its own header fields also lands here: Rebus recovered no trustedreply_serial, so the frame is dropped and the connection stays up, but the call cannot be matched to it.{:error, {:reply_dropped, :method_return}}or{:error, {:reply_dropped, {:error, error_name}}}- the peer definitely replied, but the reply exhausted a local decoding cap and was discarded. Neither is delivery-ambiguous, so decide whether to retry from what the operation does. A cap exhausted inside the reply's own header fields cannot produce these shapes; it returns{:error, :timeout}instead.{:error, :fd_claim_expired}- Rebus closed the reply's descriptors instead of handing them over.{:error, :disconnected}- the connection stopped before the reply, or before its descriptors transferred. A call carrying descriptors can return aftertimeout; see Unix file descriptor passing.- Nothing was written for
:encode_failed,:no_reply_expected,:not_connected,:serial_exhausted,:unix_fd_unsupported,:unix_fd_not_negotiated,:unix_fd_send_failed,{:invalid_message_type, type}and:remote_connection_unsupported.
Examples
message = Rebus.Message.new!(:method_call,
path: "/org/freedesktop/DBus",
interface: "org.freedesktop.DBus",
destination: "org.freedesktop.DBus",
member: "ListNames"
)
{:ok, %Rebus.Message{type: :method_return, body: [names]}} = Rebus.call(conn, message)
{:error, :timeout} = Rebus.call(conn, message, 1_000)
@spec close(pid()) :: :ok | {:error, :not_found | :remote_connection_unsupported}
Stops a local connection process created by connect/2.
Connections are supervised and, unless connect/2 was given an :owner,
remain alive after the connecting process exits. Use this function to release
a named or otherwise no-longer-needed connection. It accepts only local
connection PIDs; remote PIDs are not supported.
An owned connection is stopped the same way. Once an owner's exit has stopped
its connection there is nothing left to close, and this function answers
{:error, :not_found} as it does for any other PID that is no longer a
connection.
Return values
:ok- The supervised connection was stopped.{:error, :not_found}- The PID is not a current Rebus connection.{:error, :remote_connection_unsupported}- The PID belongs to another node.
Connects to a D-Bus endpoint and returns its connection process.
The call blocks until the connection is usable: authenticated and, on a
message bus, holding its unique name. The returned PID is supervised and
outlives the process that connected it, so release it with close/1, or
bind it to a process with :owner.
Addresses
:system- the system bus.:session- the session bus.%{family: :local, path: "/tmp/my-dbus"}- a Unix domain socket.%{family: :inet, addr: {127, 0, 0, 1}, port: 12345}- a TCP endpoint, with:inet6and an eight-element address for IPv6.
:system reads DBUS_SYSTEM_BUS_ADDRESS, then the :system_bus_address
config key, then the default unix:path=/run/dbus/system_bus_socket.
:session reads DBUS_SESSION_BUS_ADDRESS, then falls back to
unix:path=$XDG_RUNTIME_DIR/bus when XDG_RUNTIME_DIR is set.
An empty variable counts as unset. Both hold a D-Bus address list whose supported
entries are tried in the order they are listed, and a guid= on the entry
that answers must match the server's identity. See
Authentication for the rest.
Options
| Option | Default | Bounds / meaning |
|---|---|---|
:timeout | 5000 | Positive milliseconds for setup: the identity lookup, any DNS lookup, the socket connect and authentication. It bounds nothing after that. |
:read_timeout | 5000 | Positive milliseconds for the initial Hello reply, and afterwards for gaps between inbound fragments. When given it also replaces :timeout for setup. |
:write_timeout | 5000 | Positive milliseconds an outbound frame, including every authentication write, may wait for the socket to accept it. |
:name | nil | Atom to register the connection process under, for local discovery only. |
:allow_anonymous | false | Allow the ANONYMOUS mechanism, which authenticates nothing and also requires bus: false. |
:bus | true | Pass false for a peer-to-peer endpoint, which sends no Hello and has no unique name. |
:owner | nil | A local PID whose exit stops the connection. nil leaves the connection alive until close/1. |
Notes
- A PID found by name before its
connect/2returns is still being established. Operations sent to it may time out, and are safe to retry onceconnect/2succeeds. - An
:owneris monitored, not linked, and need not be the process callingconnect/2. The connection stops with{:shutdown, :owner_down}when the owner exits for any reason, including:normal. The stop is an ordinary one, so the connection's socket and any retained descriptors are closed and the bus releases the state that connection held. Inflight callers receive{:error, :disconnected}. Only a PID on the local node is accepted; a remote PID is:invalid_owner. - The supervisor owns the connection until
close/1or, with:owner, until that process exits. - A write timeout that accepted no bytes fails only that caller. After a
partial frame the connection terminates and inflight callers receive
{:error, :disconnected}. - For an address list, setup shares one budget across every candidate, so a slow candidate leaves less time for the entries after it.
- Failed address-list attempts are logged at debug level, without any address, host, path or GUID.
Return values
Success is {:ok, pid}. Every failure is {:error, reason}:
- Invalid option:
:invalid_timeout,:invalid_read_timeout,:invalid_write_timeout,:invalid_allow_anonymous,:invalid_bus_option,:invalid_owner,:invalid_name. - Owner that exited before the connection was established:
:owner_down. - Unusable address:
{:invalid_bus_address, reason},:unsupported_bus_transport,{:tcp_resolution_failed, reason},:no_system_bus_address,:no_session_bus_address. - Refused authentication:
:auth_id_unavailable,:auth_cookie_unavailable,:auth_failed,{:auth_rejected, mechanisms},:guid_mismatch. See Authentication. - Expired setup budget:
:read_timeout, or{:read_timeout, reason}once an address-list attempt has already failed. - Refused or unusable
Helloreply:{:hello_failed, reason}. - Taken
:name:{:name_taken, pid}for another Rebus connection, or{:name_registered, pid}for any other process. - Any other socket or setup failure: the failure's own atom.
Examples
# A custom Unix socket
{:ok, conn} = Rebus.connect(%{family: :local, path: "/tmp/my-dbus"})
# A TCP endpoint
address = %{family: :inet, addr: {127, 0, 0, 1}, port: 12345}
{:ok, conn} = Rebus.connect(address)
# Release a named connection when its lifecycle is complete
{:ok, conn} = Rebus.connect(address, name: :local_bus)
:ok = Rebus.close(conn)
# Let the connection end with the process whose bus-side state it holds
{:ok, conn} = Rebus.connect(address, owner: self())
Same as connect/2, but raises ArgumentError on failure.
The exception message carries the reason connect/2 would have returned.
@spec delete_signal_handler(pid(), reference()) :: :ok | {:error, :not_connected | :timeout | :disconnected}
Stops a signal handler registered by add_signal_handler/1.
The handler receives no further signals; others on the same connection carry on. Deleting a reference is idempotent while the connection is available.
Return values
:ok- the handler is gone.{:error, :not_connected}- connection setup has not completed.{:error, :timeout}- the connection did not service the request in time.{:error, :disconnected}- the connection has stopped.
Examples
{:ok, ref} = Rebus.add_signal_handler(conn)
:ok = Rebus.delete_signal_handler(conn, ref)
@spec remove_match(pid(), reference(), non_neg_integer()) :: :ok | {:error, match_error_reason()}
Removes a match-rule subscription reference.
Removing a reference is idempotent and scoped to its own connection. A removed reference receives no further signals, and the last reference for a rule also removes the rule from the bus. A removal that times out or fails keeps the reference for a retry, while Rebus clears the rule in the background. Rebus removes the reference and the bus rule when the owning process exits. Closing the connection discards both.
Return values
Success is :ok. Failures are the {:error, reason} shapes listed for
add_match/3, apart from :not_a_bus and :sender_routing_ambiguous.
@spec send(pid(), Rebus.Message.t()) :: :ok | {:error, error_reason()}
Sends a message without waiting for a reply.
Use this for signals and for method calls flagged :no_reply_expected.
send/2 allows five seconds for the connection to accept the message;
send/3 takes that timeout as an argument.
Return values
:ok- the frame was handed to the socket.{:error, :timeout}- the message may already have reached the peer. A message sent to a named PID whoseconnect/2has not yet returned was never written and is safe to retry.{:error, :disconnected}- the connection stopped.- Nothing was written for
:encode_failed,:reply_expected,{:invalid_message_type, type},:not_connected,:serial_exhausted,:unix_fd_unsupported,:unix_fd_not_negotiated,:unix_fd_send_failedand:remote_connection_unsupported.
@spec send(pid(), Rebus.Message.t(), non_neg_integer()) :: :ok | {:error, error_reason()}
Sends a message with a custom dispatch timeout in milliseconds.
Accepts the same messages as send/2 and returns the same values. The
timeout bounds how long the connection has to accept the message.
{:error, :timeout} is delivery-ambiguous.