Rebus.Service (rebus v0.4.0)
View SourceExports objects on a connection and answers the method calls made on them.
An object is a set of Rebus.Interface declarations exported at an object
path. Each export names a handler process, which receives every call made
on the export's interfaces as a message:
{export_ref, %Rebus.Service.Call{}}The connection process never runs application code. It routes each inbound call, checks it against the declarations, and refuses anything that does not match with a standard D-Bus error, so a handler only ever sees calls to declared methods with arguments of the declared types. Calls to one handler arrive in the order they were received.
Example
calculator =
Rebus.Interface.new!("org.example.Calculator",
methods: [{"Add", in: [a: "i", b: "i"], out: [sum: "i"]}]
)
{:ok, conn} = Rebus.connect(:session)
{:ok, ref} = Rebus.Service.export(conn, "/org/example/Calculator", [calculator])
receive do
{^ref, %Rebus.Service.Call{member: "Add", body: [a, b]} = call} ->
:ok = Rebus.Service.reply(call, [a + b])
endClients usually find a service by a well-known name. Export its objects
first, then request the name with Rebus.Bus.request_name/4, so that no
client reaches the name before the objects are there to answer.
Answering calls
Answer every call exactly once, with reply/3, reply_error/4 or
reply_message/3. Any process holding the Rebus.Service.Call can answer
it, so a handler can hand a slow call to another process, but the call
stays bound to its export: when the export goes away, the call is failed.
A call whose caller asked for no reply has no_reply?: true. Answering it
sends nothing and returns :ok, so a handler can treat every call the same
way. For such a call that carries descriptors, the answer is also what
releases them.
A second answer to the same call returns {:error, :unknown_call}, as does
an answer to a call that has expired or whose export is gone. The caller
never receives two replies.
What callers see
The connection answers these itself, each with a fixed message text that never echoes the call:
| Condition | Error name |
|---|---|
| Nothing exported at the path or below it | org.freedesktop.DBus.Error.UnknownObject |
A path with exports only below it, for anything but Introspect | org.freedesktop.DBus.Error.UnknownInterface |
| Interface not exported on the object | org.freedesktop.DBus.Error.UnknownInterface |
| Method not declared, or, without an interface, declared by no exported interface or by more than one | org.freedesktop.DBus.Error.UnknownMethod |
| Argument signature differs from the declaration | org.freedesktop.DBus.Error.InvalidArgs |
Property not declared by the interface a Properties call names | org.freedesktop.DBus.Error.UnknownProperty |
Set of a read-only property | org.freedesktop.DBus.Error.PropertyReadOnly |
Get of a write-only property | org.freedesktop.DBus.Error.InvalidArgs |
Set value of a type other than the declared one | org.freedesktop.DBus.Error.InvalidArgs |
:max_pending_calls calls already waiting | org.freedesktop.DBus.Error.LimitsExceeded |
| Handler exited with the call pending | org.freedesktop.DBus.Error.Failed |
| Object unexported with the call pending | org.freedesktop.DBus.Error.Failed |
No answer within the export's :call_timeout | org.freedesktop.DBus.Error.NoReply |
A caller that asked for no reply gets none of these.
org.freedesktop.DBus.Peer is answered by the connection on every path, so
it cannot be exported, and neither can
org.freedesktop.DBus.Introspectable or org.freedesktop.DBus.Properties,
which the connection reserves for itself.
Properties
The connection checks every org.freedesktop.DBus.Properties call against
the declaration of the interface it names, and sends it to the handler that
exported that interface. The call arrives with
interface: "org.freedesktop.DBus.Properties", member "Get", "Set"
or "GetAll", and the body as sent, whose first element names the
interface. By then the interface is exported on the object, a Get or
Set names a declared property that can be read or written, and a Set
value has the declared type, so the handler only reads or writes values:
alias Rebus.Service.Call
receive do
{^ref, %Call{member: "Get", body: [_iface, "Precision"]} = call} ->
Rebus.Service.reply(call, [{"u", precision}])
{^ref, %Call{member: "Set", body: [_iface, "Precision", {"u", value}]} = call} ->
Rebus.Service.reply(call, [])
{^ref, %Call{member: "GetAll"} = call} ->
values = Rebus.Interface.wrap_properties!(settings, %{"Precision" => precision})
Rebus.Service.reply(call, [values])
endThe call's :out_signature is "v" for Get, so the value is a
{signature, value} variant; "" for Set; and "a{sv}" for GetAll,
which Rebus.Interface.wrap_properties/2 builds from the declaration. A
GetAll for an interface that declares no readable property is answered
with an empty dictionary without reaching the handler.
Signals
emit/6 sends a signal from an exported object, typed by the interface
declaration, and properties_changed/6 sends
org.freedesktop.DBus.Properties.PropertiesChanged for properties that
changed, as each property's emits_changed allows:
:ok = Rebus.Service.emit(conn, "/org/example/Calculator", calculator, "Computed", [5])
:ok =
Rebus.Service.properties_changed(conn, "/org/example/Calculator", settings,
%{"Precision" => 3}, [])Introspection
The connection answers org.freedesktop.DBus.Introspectable.Introspect
itself, with XML generated from the exported declarations, so tools such
as busctl and d-feet can find and describe the objects. An exported
object lists its own interfaces, then Peer, Introspectable and
Properties, then the next path element of every export below it. A path
with exports only below it, such as / or /org for
/org/example/Calculator, lists Introspectable and those child elements,
so a tool can walk the tree from /. A path served by a subtree export
lists the subtree's interfaces and no children, since the connection
cannot know which paths the handler serves.
Unix file descriptors
Descriptors sent with a call arrive in the call's :unix_fds as
Rebus.UnixFD handles, indexed by its h arguments. The connection owns
them and the handler borrows them while the call is pending: it may use
them, with Rebus.UnixFD.with_fd/2, but only the connection can close them.
When the call completes, however it completes, the connection closes each
one it still owns, so a handler that ignores them leaks nothing. To keep
them, take ownership with take_fds/2 before the call completes:
receive do
{^ref, %Rebus.Service.Call{member: "TakeLog"} = call} ->
{:ok, [handle]} = Rebus.Service.take_fds(call)
:ok = Rebus.Service.reply(call, [])
# handle is now the handler's, closed when the handler exits
# or with Rebus.UnixFD.close/1
endA reply sends descriptors through reply/3's :fds option, or the
unix_fds of a message answered with reply_message/3: handles, raw
descriptor numbers, or both, as for Rebus.Message.new/2. A reply may
carry the call's own borrowed handles. Answering the call closes them, but
the connection keeps their descriptors open until the reply has passed
them on:
receive do
{^ref, %Rebus.Service.Call{member: "Echo", unix_fds: [handle]} = call} ->
:ok = Rebus.Service.reply(call, [0], fds: [handle])
endSee Unix file descriptor passing for the full ownership rules.
Summary
Types
A reason why export/4 refused an export.
A reason why a reply was not sent.
A reason why take_fds/2 did not return a call's handles.
Functions
Emits the signal member of interface from the object at path.
Exports interfaces at path on conn.
Emits org.freedesktop.DBus.Properties.PropertiesChanged for properties
of interface on the object at path.
Answers a call with a method return carrying body.
Answers a call with a D-Bus error named name and carrying message.
Answers a call with a reply built by hand.
Takes ownership of the handles that arrived with a call.
Removes an export.
Types
@type export_error() :: :not_connected | :disconnected | :timeout | :remote_connection_unsupported | :invalid_path | :invalid_handler | :invalid_subtree | :invalid_call_timeout | {:invalid_interface, term()} | {:already_exported, path :: binary(), interface :: binary()}
A reason why export/4 refused an export.
{:invalid_interface, reason} carries the offending value, or
{:duplicate, name} for an interface listed twice, or {:reserved, name}
for an interface the connection implements itself.
@type reply_error() :: :unknown_call | :signature_mismatch | Rebus.Message.construction_error() | Rebus.error_reason()
A reason why a reply was not sent.
@type take_fds_error() :: :unknown_call | :timeout | :disconnected
A reason why take_fds/2 did not return a call's handles.
Functions
@spec emit( pid(), binary(), Rebus.Interface.t() | binary(), binary(), [term()], keyword() ) :: :ok | {:error, :unknown_signal | :signature_required | {:invalid_option, term()} | Rebus.Message.construction_error() | Rebus.error_reason()}
Emits the signal member of interface from the object at path.
Given a Rebus.Interface, the signal must be one it declares, and body
is encoded with the declared signature. Given an interface name, the
signature must be passed as the :signature option. The signal is sent
as Rebus.send/3 sends any message: the connection does not check that
anything is exported at path.
Options
:signature- the body's signature. Required with an interface name, and refused with aRebus.Interface, which supplies its own.:destination- the unique name to send the signal to, rather than broadcasting it.:flagsand:fds- as forRebus.Message.new/2.:timeout- milliseconds to wait for the connection to accept the signal. Defaults to5000.
Return values
:ok- the signal was handed to the socket.{:error, :unknown_signal}- the interface does not declaremember.{:error, :signature_required}- an interface name without:signature.{:error, {:invalid_option, opts}}- an option not listed above.- Any error of
Rebus.Message.new/2, such as{:error, :invalid_body}for a body that does not fit the signature, or ofRebus.send/3.
@spec export(pid(), binary(), [Rebus.Interface.t()], keyword()) :: {:ok, reference()} | {:error, export_error()}
Exports interfaces at path on conn.
Calls on those interfaces at path are sent to the handler as
{ref, %Rebus.Service.Call{}}, where ref is the reference returned
here. Several exports can share a path, each with its own interfaces and
handler, but an interface can be exported only once at a path.
The handler is monitored. When it exits, everything it exported is
unexported, and each call still waiting for its answer is failed with
org.freedesktop.DBus.Error.Failed. Exports also end with the connection.
Options
:handler- the local process that receives the calls. Defaults to the caller.:subtree- whentrue, the export also serves every path belowpath, and the call's:pathis the path actually called. An exact export of a path takes precedence over a subtree that covers it, and the longest covering subtree takes precedence over shorter ones. Defaults tofalse.:call_timeout- positive milliseconds a call may wait for its answer before the connection answers the caller withorg.freedesktop.DBus.Error.NoReply, or:infinity. Defaults to60000.:timeout- milliseconds to wait for the connection to accept the export. Defaults to5000.
Return values
{:ok, ref}- the export's reference, which tags its calls and is whatunexport/3takes.{:error, :invalid_path}-pathis not a valid object path.{:error, {:invalid_interface, reason}}-interfacesis not a non-empty list of distinctRebus.Interfacestructs, or names an interface the connection reserves; seeexport_error/0.{:error, :invalid_handler},{:error, :invalid_subtree}and{:error, :invalid_call_timeout}- an invalid option. The handler must be a local PID.{:error, {:already_exported, path, interface}}-interfaceis already exported atpath.{:error, :not_connected},{:error, :timeout},{:error, :disconnected}and{:error, :remote_connection_unsupported}- as for
Rebus.send/3. An export that times out is withdrawn.
- as for
@spec properties_changed( pid(), binary(), Rebus.Interface.t(), %{required(binary()) => term()}, [binary()], keyword() ) :: :ok | {:error, Rebus.Interface.property_error() | {:invalid_property_change, binary()} | {:invalid_option, term()} | Rebus.Message.construction_error() | Rebus.error_reason()}
Emits org.freedesktop.DBus.Properties.PropertiesChanged for properties
of interface on the object at path.
changed maps property names to their new values, typed by the
declaration, and invalidated names properties that changed without
their values. Each property's emits_changed decides where it may appear,
as Rebus.Interface.properties_changed/5 describes: one that emits true
only in changed, one that emits :invalidates only in invalidated,
and one that emits false or :const in neither.
Takes the options emit/6 takes, except :signature, and returns what it
returns, plus Rebus.Interface.properties_changed/5's errors for a
property the interface does not declare or does not allow where it
appears.
@spec reply(Rebus.Service.Call.t(), [term()], keyword()) :: :ok | {:error, reply_error()}
Answers a call with a method return carrying body.
The reply is encoded with the method's declared out-signature, the call's
:out_signature, in the calling process, so a body that does not fit
it is reported here as {:error, :signature_mismatch} and the call stays
open for another answer.
Options
:fds- Unix file descriptors to send with the reply, whichhvalues inbodyindex:Rebus.UnixFDhandles, raw descriptor numbers, or both, as forRebus.Message.new/2. They may include the call's own borrowed handles, whose descriptors stay open until the reply is written. A handle the handler owns stays open and its own, and a raw number stays the handler's to close once this returns. Defaults to[].:timeout- milliseconds to wait for the connection to accept the reply. Defaults to5000.
Return values
Once the connection accepts a reply, the call is complete and the handles it borrowed are closed, even if writing the reply then fails. Their descriptors stay open only until the frame is written or abandoned, and the call cannot be answered again.
:ok- the reply was handed to the socket, or the caller asked for no reply and nothing was sent.{:error, :signature_mismatch}-bodydoes not fit the call's:out_signature.{:error, :unknown_call}- the call was already answered, expired, or its export is gone.{:error, :unix_fd_closed}- a handle in:fdswas already closed, or belongs to another node, when the connection accepted the reply. Nothing was sent, and the call stays open.{:error, :unix_fd_unsupported}and{:error, :unix_fd_not_negotiated}- the reply carries descriptors this connection cannot send, as for
Rebus.send/3. The call stays open.
- the reply carries descriptors this connection cannot send, as for
{:error, :unix_fd_send_failed}- the kernel refused a descriptor, so nothing was written and the caller gets no reply. The call is complete all the same and cannot be answered again.{:error, :timeout}- delivery is ambiguous, as forRebus.send/3. The call is complete either way and cannot be answered again.{:error, :disconnected}- the connection stopped.- Any other reason is a failure to build or write the frame, as for
Rebus.send/3. The call is complete unless the frame could not be built.
@spec reply_error(Rebus.Service.Call.t(), binary(), binary(), keyword()) :: :ok | {:error, reply_error()}
Answers a call with a D-Bus error named name and carrying message.
name must be a valid D-Bus error name, such as
"org.example.Error.Refused"; an invalid one returns
{:error, {:invalid_header_field, :error_name}} and leaves the call open.
Takes the same options and returns the same values as reply/3. The
body has no h values, so descriptors passed as :fds travel with the
error unindexed.
@spec reply_message(Rebus.Service.Call.t(), Rebus.Message.t(), keyword()) :: :ok | {:error, reply_error() | Rebus.Message.encoding_error()}
Answers a call with a reply built by hand.
message must be a :method_return or an :error; any other type
returns {:error, {:invalid_message_type, type}}. A method return must
have the declared out-signature, the call's :out_signature, or this returns
{:error, :signature_mismatch}. An error can carry any body. The message's
reply_serial and destination are replaced with the call's own.
The message is encoded in the calling process first, so one that cannot be
encoded returns the error Rebus.Message.encode/2 would report and leaves
the call open. Otherwise this takes the options and returns the values of
reply/3, except that there is no :fds option: the message carries its
own descriptors in unix_fds.
@spec take_fds(Rebus.Service.Call.t(), non_neg_integer()) :: {:ok, [Rebus.UnixFD.t()]} | {:error, take_fds_error()}
Takes ownership of the handles that arrived with a call.
Until a call completes, its handles are borrowed from the connection, which closes them when the call is answered, expires or fails. This makes the calling process their owner instead, so they outlive the call and close when this process closes them or exits. The call stays pending, and still has to be answered.
Return values
{:ok, handles}- the handles, the same ones as the call's:unix_fdsand in the same order, now owned by the calling process. A call that carried none returns{:ok, []}without reaching the connection.{:error, :unknown_call}- the handles were already taken, or the call was answered, expired, or its export is gone.{:error, :timeout}- the connection did not answer withintimeout. It may still make this process their owner afterwards, or it may not: either way each handle closes exactly once, when this process or the connection is done with it.Rebus.UnixFD.owner/1on the call's:unix_fdstells which once the connection has caught up, and a secondtake_fds/2answers{:error, :unknown_call}if the first one went through.{:error, :disconnected}- the connection stopped, closing the handles it still owned. Only if it was killed as it handed them over can this process have become their owner, whichRebus.UnixFD.owner/1on the call's:unix_fdstells.
@spec unexport(pid(), reference(), non_neg_integer()) :: :ok | {:error, :not_connected | :disconnected | :timeout | :remote_connection_unsupported}
Removes an export.
Each call on the export still waiting for its answer is failed with
org.freedesktop.DBus.Error.Failed, and answering it afterwards returns
{:error, :unknown_call}. Removing an export that is already gone returns
:ok.