dbus
import dbus from "dbus";
import io from "io";
var bus: dbus.Bus = dbus.session() or {
io.eprintln("no session bus: {err.desc()}");
return 1;
};
defer bus.close();
var m: dbus.Message = dbus.method_call("org.freedesktop.Notifications", "/org/freedesktop/Notifications",
"org.freedesktop.Notifications", "Notify");
m.body.text("my-app"); // app_name
m.body.uint32(0); // replaces_id
m.body.text(""); // app_icon
m.body.text("Build finished"); // summary
m.body.text("All tests passed"); // body
m.body.begin_array("s"); // actions
m.body.end_array();
m.body.begin_dict("sv"); // hints
m.body.entry_sv("urgency", &dbus.uint8(1));
m.body.end_array();
m.body.int32(-1); // expire_timeout
let reply: dbus.Message = bus.call(&m) or { io.eprintln("{err.desc()}"); return 1; };
var r: dbus.Reader = reply.reader();
let id: u32 = r.uint32() or { 0 };
io.println("notification {id}");dbus talks to the services of the Linux desktop over
D-Bus:
notifications, the desktop portal (file dialogs, opening URLs), the
Secret Service, status icons, owning a name. It is written in tape (no
libdbus) and comes in three layers:
- a
Busis a connection to the session or the system bus overnet’s Unix-domain sockets. Calls either wait for their reply (call) or are answered later (start_call); signals go to handlers by match rule; the objects you export answer calls through your handlers.pollnever waits, so auiprogram handles the bus from its event loop; - a
Connis the connection on its own: authentication,Helloand framing over bytes you feed in and send out; - the wire format:
WriterandReadermarshal values by type signature,Valueis a value as a tree,encodeanddecodeturn aMessageinto bytes and back.
Not covered: passing file descriptors (fds ride as indices in the message
but are never sent), the tcp: and launchd: transports,
authentication other than EXTERNAL, and being a bus. Abstract sockets
(the unix:abstract= address dbus-launch gives) work on Linux, through
net.unix_abstract.
Connecting
var bus: dbus.Bus = dbus.session() or { return 1; }; // DBUS_SESSION_BUS_ADDRESS, then $XDG_RUNTIME_DIR/bus
var sys: dbus.Bus = dbus.system() or { return 1; }; // /run/dbus/system_bus_socket
io.println("we are {bus.unique_name()}"); // ":1.42"
bus.close();connect(address, opts) connects to the first entry of an address list
that answers ("unix:path=/run/user/1000/bus;unix:path=/tmp/other"),
authenticates and waits for Hello’s reply; start_connect returns as
soon as the socket is open, and ready(), wait_ready(timeout) or any
call finishes the handshake (messages you send meanwhile go after
Hello). BusOptions holds the timeout (timeout, 25 s by default: the
connect, the handshake and each blocking call) and the uid to
authenticate as (uid; by default this process’s).
var opts: dbus.BusOptions = dbus.bus_options();
opts.timeout = time.seconds(5);
var b: dbus.Bus = dbus.connect("unix:path=/run/user/1000/bus", opts) or {
io.eprintln("{err.desc()}");
return 1;
};session_address() and system_address() give the address session()
and system() use. Errors: NoBus when there is no address, Io from
the socket (NotFound for a missing socket file, ConnectionRefused for
a server that has gone; Unsupported for another transport, or an
abstract socket on a platform other than Linux), AuthRejected, Wire when the
bus breaks the protocol.
A Bus is not copied: keep it in one place, a module variable when your
handlers and ui callbacks use it.
Messages
A Message has its header’s fields (kind, flags, serial,
reply_serial, path, interface, member, error_name,
destination, sender, unix_fds) and a body: m.body is a Writer
you append the arguments to, in order.
var m: dbus.Message = dbus.method_call("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
"org.freedesktop.portal.OpenURI", "OpenURI");
m.body.text(""); // parent window
m.body.text("https://tapelang.org");
m.body.begin_dict("sv"); // options: a{sv}
m.body.entry_sv("handle_token", &dbus.text("tape1"));
m.body.end_array();
io.println(m.signature()); // ssa{sv}
let s: dbus.Message = dbus.signal("/org/example/Thing", "org.example.Thing", "Changed");The Writer has one method per type: uint8 (y), boolean (b),
int16 (n), uint16 (q), int32 (i), uint32 (u), int64 (x),
uint64 (t), double (d), text (s), object_path (o),
signature_value (g), unix_fd (h), and containers:
begin_array(elem) … end_array(), begin_dict("sv") for an a{sv}
(its entries with begin_entry() … end_entry(), or entry_sv(key, &v)
for a key and a variant), begin_struct() … end_struct(),
begin_variant(sig) … end_variant(), and variant(&v) and value(&v)
for a whole Value. At the top level the signature is built
as you write; inside an array or a variant each value must be the
declared type, and a value of another type traps (a bug in your code).
Text that is not UTF-8, or a bad object path, is remembered instead
(w.error()) and makes the message fail to send. In a string literal a
{ starts an interpolation, so a dict’s signature is written "a\{sv}":
begin_dict("sv") saves you the braces.
method_return(&call) and error_reply(&call, name, text) answer a
call; m.describe() is a line for logs as dbus-monitor prints it;
m.expects_reply() says whether the sender waits for one.
m.encode() gives the bytes (after serial is set; Bus and Conn set
it), dbus.decode(bytes) reads a message back and checks all of it, and
dbus.message_size(bytes) says how long the message the bytes start with
is (null before 16 bytes have come):
m.serial = 7;
let bytes = m.encode() or { return 1; };
let back: dbus.Message = dbus.decode(bytes.slice()) or { return 1; };
io.println(back.describe());Reading a message
m.reader() reads the body in order, each read naming the type it
expects: another type there is an error (TypeMismatch), never a trap.
var r: dbus.Reader = reply.reader();
let uris: dbus.Value = r.value() or { return 1; }; // a whole value as a tree
while (r.peek() != 0) { r.skip() or { return 1; }; }text, object_path and signature_value return views into the message
(valid while it is). Arrays: begin_array(), then read elements while
more(), then end_array() (which skips what you did not read). Structs
and dict entries: begin_struct() / begin_entry() … end_struct() /
end_entry(). Variants: begin_variant() returns the value’s signature.
peek() is the next value’s type code (0 at the end of a container),
peek_signature() its whole type, at_end() whether nothing is left;
values() reads every value left as trees, and m.values() the whole
body. dbus.reader(data, sig, big) reads any bytes you have.
Values
A Value is any D-Bus value as a tree: Byte, Bool, Int16 …
Uint64, Double, Str, ObjectPath, Signature, UnixFd, Array
(with its element signature), Struct, Entry (a dict entry) and
Variant.
var hints: dbus.Value = dbus.dict("sv"); // a{sv}
hints.put("urgency", dbus.uint8(2)); // wrapped in a variant
hints.put("category", dbus.text("im.received"));
io.println(hints.signature()); // a{sv}
let u: i64 = hints.lookup("urgency")!.as_i64()!; // 2: lookup unwraps the variant
var pt: dbus.Value = dbus.structure();
pt.push(dbus.int32(3));
pt.push(dbus.int32(4)); // (ii)
var list: dbus.Value = dbus.array("(ii)");
list.push(pt);
m.body.value(&list);Constructors: uint8, boolean, int16, uint16, int32, uint32,
int64, uint64, double, text, object_path, signature,
unix_fd, array(elem), dict(kv), structure(), entry(k, v),
variant(v). Reading one: as_text() (s, o, g), as_i64() (any
integer that fits), as_u64(), as_bool(), as_f64(), items() (an
array’s items, a struct’s fields, an entry’s key and value), inner() (a
variant’s value), lookup(key) (in an a{s?}), signature(), and
equals(&other).
Calling
let reply: dbus.Message = bus.call(&m) or {
io.eprintln("{err.name()}: {err.desc()}"); // an error reply is Remote: its name and message
return 1;
};
let quick: dbus.Message = bus.call(&m, time.millis(500)) or { return 1; }; // TimedOut after itcall sends and waits for the reply (the bus’s timeout, or yours).
Messages that arrive while it waits are kept for poll, where their
handlers run.
start_call returns at once with the call’s serial; the reply goes to
your handler when poll sees it (or a org.freedesktop.DBus.Error.NoReply
error after the timeout):
fn on_reply(ctx: *u8, bus: *dbus.Bus, reply: *const dbus.Message) {
if (reply.kind == dbus.MessageType.Error) {
io.println("failed: {reply.error_name}");
return;
}
var r: dbus.Reader = reply.reader();
let id: u32 = r.uint32() or { 0 };
io.println("id {id}");
}
bus.start_call(&m, on_reply, null) or { return 1; };With a null handler the reply comes out of poll instead (match it by
reply_serial); cancel(serial) forgets a started call. send sends
without waiting for anything (a signal, a reply, a call whose answer you
don’t need); reply_error(&call, name, text) answers a call with an
error.
Signals
fn on_closed(ctx: *u8, bus: *dbus.Bus, m: *const dbus.Message) {
var r: dbus.Reader = m.reader();
let id: u32 = r.uint32() or { 0 };
io.println("notification {id} closed");
}
let id: i64 = bus.subscribe(dbus.signal_rule("org.freedesktop.Notifications", "NotificationClosed"),
on_closed, null) or { return 1; };
bus.unsubscribe(id) or {};A Rule says which messages you want: kind (signals by default),
sender, interface, member, path, path_namespace (a path and
everything under it), destination, arg0 (the first argument, a
string). subscribe asks the bus for them (AddMatch) and calls your
handler for each one poll sees; add_match and remove_match only
ask the bus, and the messages come out of poll. rule.text() is the
rule as the bus reads it, rule.matches(&m) checks a message.
The desktop portal
A portal call returns a Request object’s path at once, and its answer
comes later as that object’s Response signal. The path is known before
the call (from your unique name and the handle_token you pass), so
subscribe first:
let path: string = dbus.portal_request_path(bus.unique_name(), "tape1");
var rule: dbus.Rule = dbus.signal_rule("org.freedesktop.portal.Request", "Response");
rule.path = path;
bus.subscribe(rule, on_response, null) or { return 1; }; // on_response reads "ua{sv}": 0 is success
// then the call, with options a{sv} holding handle_token: "tape1"portal_call does all of it and waits for the answer:
let token: string = bus.portal_token();
var m: dbus.Message = dbus.method_call(dbus.PORTAL_NAME, dbus.PORTAL_PATH,
"org.freedesktop.portal.FileChooser", "OpenFile");
m.body.text(""); // the parent window
m.body.text("Attach");
m.body.begin_dict("sv");
m.body.entry_sv("handle_token", &dbus.text(token));
m.body.end_array();
let r: dbus.PortalResponse = bus.portal_call(&m, token, null) or {
if (err.service_missing()) { io.println("no portal"); }
return 1;
};
if (r.response == 0) { io.println(r.results.lookup("uris")!.items()[0].as_text()!); }For any other signal that answers a call (a Secret Service prompt’s
Completed), catch(rule) before the call and wait_caught(id, timeout) after it. err.service_missing() says the service is not
there at all (no bus, or no one owns its name).
The notify, dialog, secrets,
tray and launch modules use dbus on Linux;
a program calls them rather than these services itself.
In a ui program
poll reads what has come and runs the handlers without waiting, so a
ui program watches the bus’s socket and dispatches when it is
readable:
var g_bus: dbus.Bus;
fn on_bus(h: i64) {
g_bus.dispatch() or { ui.unwatch_socket(h); };
}
g_bus = dbus.session() or { return 1; };
ui.watch_socket(g_bus.handle(), net.READABLE, on_bus) or { return 1; };A component puts ui.Watch { handle: g_bus.handle(); want: net.READABLE; on_ready: on_bus_ready; } in its view instead. dispatch() polls until
nothing is left and drops what no handler took; poll() returns those
messages to you one by one, and wait(timeout) waits for one. After a
blocking call, the messages it kept may not wake the loop (the socket
has already been read): buffered() is then true, so call dispatch()
after a call.
The shared session connection
The desktop modules (notify, dialog,
secrets, tray, launch) share
one connection to the session bus, and your program can use it too
rather than opening its own:
fn bus_watch(h: i64) { ui.watch(h, dbus.shared_ready) or {}; }
fn bus_unwatch(h: i64) { ui.unwatch(h); }
let bus: *dbus.Bus = dbus.shared_session() or { return 1; };
bus.subscribe(dbus.signal_rule("org.example.Player", "Changed"), on_changed, null) or { return 1; };
let hold: i64 = dbus.shared_hold(bus_watch, bus_unwatch); // dispatched in the ui loop from now on
// ...
dbus.shared_release(hold);| Function | Does |
|---|---|
shared_session() -> *Bus | The connection, made the first time (and again after it failed or closed) |
shared_generation() -> i64 | How many connections it has made: a new one has none of the old one’s subscriptions or exports, so make them again when this changes |
shared_open() -> bool | It is there and alive (nothing connects) |
shared_hold(watch, unwatch) -> i64 | Have it dispatched in the ui loop: your watch(h) registers ui.watch(h, dbus.shared_ready) (ui.watch_socket on Windows), unwatch(h) removes it. However many hold it, it is watched once; the last shared_release removes the watch |
shared_ready(h) | The watch’s callback: dispatches it (every user’s handlers run) and makes it again if it has gone |
shared_wake(), shared_wake_handle() | Make shared_ready run at the loop’s next turn; the handle that does it |
shared_close() | End it (the next shared_session makes another) |
The rules that let several users share it:
- Use handlers. Whoever dispatches it runs everybody’s handlers and
drops what no handler takes, so subscribe, export,
catchor start calls with anon_reply; don’t wait forpollto hand you a message. - Export at paths of your own: two exports at one path and interface are one.
- A handler doesn’t block. It may
send(a reply, a signal), but acallorwait_caughtinside a handler would dispatch the connection inside its own dispatch. Queue the work and do it from the ui loop. - Never close it.
bus.close()traps on it; it belongs todbus. - Blocking calls lose nothing. A file dialog or a keyring prompt
waits on it for as long as the user takes, dispatching as it waits: a
tray menu still answers, a notification’s click is queued. What a
callkeeps while it waits is dispatched at the loop’s next turn. - The main thread’s. It is a module variable of
dbus, which a thread fn can’t reach.
Exporting objects
An Interface at a path answers the calls made to it through your
handlers. Introspect, Peer.Ping and the Properties interface are
answered for you:
var g_tip: string = "3 unread";
fn item_call(ctx: *u8, bus: *dbus.Bus, call: *const dbus.Message) -> bool {
if (call.member != "Activate") { return false; } // UnknownMethod is sent
var r: dbus.Message = dbus.method_return(call);
bus.send(&r) or {};
return true;
}
fn item_get(ctx: *u8, name: []const u8, w: *dbus.Writer) -> bool {
if (name == "Title") { w.variant(&dbus.text("My app")); return true; }
if (name == "Status") { w.variant(&dbus.text("Active")); return true; }
return false; // UnknownProperty
}
let props: [2][]const u8 = ["Title", "Status"];
bus.export(dbus.Interface {
path: "/StatusNotifierItem"; name: "org.kde.StatusNotifierItem";
xml: " <interface name=\"org.kde.StatusNotifierItem\">...</interface>\n";
properties: props[0..2]; on_call: item_call; on_get: item_get; .. });
bus.request_name("org.kde.StatusNotifierItem-4242-1", 0) or { return 1; };on_call replies (send of a method_return, or reply_error; now or
later) and returns true, or returns false for a method it does not have.
on_get writes a property as one variant; on_set(ctx, name, &value)
sets one (without it every property is read-only); properties are the
names GetAll reports, xml the interface’s part of the introspection
data, ctx your pointer for the handlers. properties_changed(path, name, names) emits PropertiesChanged with the values on_get gives
now. unexport(path, name) removes an interface. request_name(name, flags) and release_name(name) own a well-known name (NAME_PRIMARY_OWNER,
NAME_IN_QUEUE, NAME_EXISTS, NAME_ALREADY_OWNER; flags
NAME_ALLOW_REPLACEMENT, NAME_REPLACE_EXISTING, NAME_DO_NOT_QUEUE).
Addresses
let list = dbus.parse_address("unix:abstract=/tmp/dbus-1,guid=7f;unix:path=/run/user/1000/bus") or { return 1; };
io.println(list.get_ref(1).get("path")!); // /run/user/1000/bus
let ep: net.Endpoint = list.get_ref(1).endpoint() or { return 1; };An Address is a transport and its keys (get(key), values unescaped);
endpoint() is where a unix:path= (or unix:runtime=yes) address
connects.
The connection engine
Conn is the connection without a socket: it authenticates (AUTH EXTERNAL with your uid in hex), says Hello, numbers and frames
messages. You send output(), call advance_output(n) for what was
sent, feed what arrives, and take messages from next():
var c: dbus.Conn = dbus.conn(dbus.ConnOptions { uid: 1000; .. });
// write c.output() to your transport: "\0AUTH EXTERNAL 31303030\r\n"
c.advance_output(c.output().len);
c.feed(received) or { return 1; }; // "OK <guid>\r\n": BEGIN and Hello are queued
let serial: u32 = c.send(&m) or { return 1; };
let next: ?dbus.Message = c.next() or { return 1; };phase() is Auth, Hello, Open or Closed; unique_name() the
name Hello gave, guid() the server’s. ConnOptions has uid (-1
sends no identity), unix_fds (negotiate fd passing; unix_fds() says
whether it was agreed), hello (false for a peer-to-peer connection) and
max_message. end_of_input() says the transport ended; has_message()
says a whole message waits. auth_identity(uid) is the hex the
authentication sends.
Errors
The wire’s problems are WireError values (Truncated, BadEndian,
BadVersion, BadType, UnknownType, TooLarge, BadPadding,
BadBool, BadString, BadObjectPath, BadSignature, BadName,
MissingField, BadField, BadArray, TypeMismatch, TooDeep,
BadSerial, BodyMismatch, BadAuth, BadAddress), each with a
description. A Conn or Bus fails with a DbusError: Io { e },
Wire { e }, AuthRejected, Remote { name, message } (an error reply;
remote_error(&m) makes one from a message), Disconnected, NoBus;
desc() reads it and name() is a Remote’s error name. Once the
connection fails, every later call returns the same error.
valid_signature, single_type, single_element, valid_object_path,
valid_interface, valid_member and valid_bus_name check text by the
specification’s rules. The limits are MAX_MESSAGE (128 MiB),
MAX_ARRAY (64 MiB), MAX_SIGNATURE (255) and MAX_DEPTH (64); the
header flags NO_REPLY_EXPECTED, NO_AUTO_START and
ALLOW_INTERACTIVE_AUTHORIZATION; BUS_NAME, BUS_PATH and
BUS_INTERFACE are the bus’s own.
One thread uses a Bus or a Conn at a time.
Last modified: