lsdbus is a simple to use D-Bus binding for Lua based on the sd-bus and sd-event APIs.
Table of Contents
- Installing
- Quickstart
- Usage
- API
- Internals
- Tests
- License
- FAQ
- ChangeLog
- References
First, ensure that the correct packages are installed. For example:
$ sudo apt-get install cmake lua5.3 liblua5.3-dev libsystemd-dev libmxml-dev
To run the tests, install lua-unit
or install it directly from here
[2]. Then build via CMake:
$ cd lsdbus
$ mkdir build && cd build
$ cmake .. && make
...
$ sudo make install
If you have multiple Lua versions installed, you can force the one to
be used by setting CONFIG_LUA_VER
(or pass -DCONFIG_LUA_VER=X
to
cmake) to one of 5.4
, 5.3
, 5.2
, 5.1
or jit
.
Server
$ lua -l lsdbus
Lua 5.3.6 Copyright (C) 1994-2020 Lua.org, PUC-Rio
> b=lsdbus.open()
> b:request_name("lsdbus.test")
> intf = { name="lsdbus.testif", methods={ Hello={ handler=function() print("hi lsdbus!") end }}}
> s = lsdbus.server.new(b, "/", intf)
> b:loop()
Client (open a second terminal)
$ lua -l lsdbus
Lua 5.3.6 Copyright (C) 1994-2020 Lua.org, PUC-Rio
> b = lsdbus.open()
> p = lsdbus.proxy.new(b, "lsdbus.test", '/', "lsdbus.testif")
> p
srv: lsdbus.test, obj: /, intf: lsdbus.testif
Methods:
Hello () ->
> p('Hello')
> p('Hello')
Before doing anything, it is necessary to connect to a bus:
lsdb = require("lsdbus")
b = lsdb.open()
To faciliate testing message conversion, lsdbus provides the testmsg
function, that accepts a D-Bus message specifier (see
sd_bus_message_append(3)
and a value, creates a D-Bus message from
it and converts it back to Lua (the example below uses the small
uutils
[2] library for printing tables):
> lsdb = require("lsdbus")
> u = require("utils")
> b = lsdb.open()
> u.pp(b:testmsg('u', 33))
33
> u.pp(b:testmsg('ai', {1,2,3,4}))
{1,2,3,4}
> u.pp(b:testmsg('a{s(is)}', { one = {1, 'this is one'}, two = {2, 'this is two'} }))
{one={1,"this is one"},two={2,"this is two"}}
> u.pp(b:testmsg('a{sv}', { foo={'s', "nirk"}, bar={'d', 2.718}, dunk={'b', false}}))
{foo="nirk",dunk=false,bar=2.718}
Type conversions
D-Bus Type | D-Bus specifier | Lua | Example Lua to D-Bus msg... | ...and back to Lua |
---|---|---|---|---|
boolean | b |
boolean |
'b', true |
true |
integers | y , n , q , i , u , x , t |
number (integer) |
'i', 42 |
42 |
floating-point | d |
number (double) |
'd', 3.14 |
3.14 |
file descriptor | h |
number |
||
string | s |
string |
's', "foo" |
"foo" |
signature | g |
string |
'g', "a{sv}" |
"a{sv}" |
object path | o |
string |
'o', "/a/b/c" |
"/a/b/c" |
variant | v |
{SPECIFIER, VALUE} |
'v', {'i',33 } |
33 (or {'i',33} with callr ) |
array | a |
table (array part) |
'ai', {1,2,3,9} |
{1,2,3,9} |
struct | (...) |
table (array part) |
'(ibs)', {3, false, "hey"} |
{3, false, "hey"} |
dictionary | a{...} |
table (dict part) |
'a{si}', {a=1, b=2} |
{a=1, b=2} |
Notes:
- More examples can be found in the unit tests:
test/message.lua
- Variant is the only type whose conversion is asymmetrical,
i.e. the input arguments are not equal the result. This is because
the variant is unpacked automatically. However, in some cases it is
desirable to get the variant in its raw table form (e.g. when the
identical value needs to be returned). For that case, the "raw"
methods
callr
(andtestmsgr
) can be used. - the tables of deserialized arrays, stucts and variants each have a
metatable with the
__name
field set to the respective type. This permits identifying the original type after the conversion to Lua.
There are two client APIs: the high level lsdbus.proxy
API uses
introspection to create a proxy object, that can be used to
conveniently call methods or get/set properties. The plumbing API
allows the same, however more arguments (destination, path, interface,
member, typestring and args) need to be provided.
Example
> lsdb = require("lsdbus")
> b = lsdb.open("system")
> td = lsdb.proxy.new(b, 'org.freedesktop.timedate1', '/org/freedesktop/timedate1', 'org.freedesktop.timedate1')
> print(td)
srv: org.freedesktop.timedate1, obj: /org/freedesktop/timedate1, intf: org.freedesktop.timedate1
Methods:
SetLocalRTC (bbb) ->
SetTime (xbb) ->
SetTimezone (sb) ->
SetNTP (bb) ->
ListTimezones () -> as
Properties:
Timezone: s, read
LocalRTC: b, read
NTP: b, read
TimeUSec: t, read
NTPSynchronized: b, read
RTCTimeUSec: t, read
CanNTP: b, read
Signals:
Properties can be accessed by indexing the proxy:
> td.Timezone
Pacific/Tahiti
Methods can called by "calling" the proxy and providing the Method name as the first argument:
> td('ListTimezones')
Unlike with the low-level bus:call
, no D-Bus types need to be
provided.
Syntax
ret, res0, ... = bus:call(dest, path, intf, member, typestr, arg0...)
in case of success ret
is true
and res0
, res1
, ... are the
return values of the call.
if case of failure ret
is false
and res0
is a table of the form
{ error, message }
.
Example
lsdb = require("lsdbus.core")
b = lsdb.open('default_system')
u = require("utils")
-- calling a valid method
u.pp(b:call('org.freedesktop.timedate1', '/org/freedesktop/timedate1', 'org.freedesktop.timedate1', 'ListTimezones')})
true, {"Africa/Abidjan","Africa/Accra","Africa/Addis_Ababa", ...
-- calling an invalid method
u.pp(b:call('org.freedesktop.timedate1', '/org/freedesktop/timedate1', 'org.freedesktop.timedate1', 'NoMethod'))
false, {"org.freedesktop.DBus.Error.UnknownMethod","Unknown method NoMethod or interface org.freedesktop.timedate1."}
b:emit_signal(PATH, INTERFACE, MEMBER, TYPESTR, ARG0...)
Example
b:emit_signal("/foo", "lsdbus.foo.bar0", "Alarm", "ia{ss}", 999, {x="one", y="two"})
The event loop can be entered and exited by calling
b:loop()
-- and
b:exit_loop()
respectively. Alternatively,
b:run(usec)
allows running the loop for a limited time (see sd_event_loop(3)
,
sd_event_run(3)
and sd_event_exit(3)
.)
Any callback can use the method b:context()
to retrieve additional
information which depending on the callback type may include
interface
path
member
sender
destination
(these are retrieved using sd_bus_get_current_message(3)
and the
respective sd_bus_message_get_*(3)
functions).
Server object callbacks can be registered with
vtab=lsdbus.server.new
. The interface uses the same Lua
representation of the D-Bus interface XML (see below) decorated with
callback functions:
local interface_table = {
name="foo.bar.interface0",
methods = {
Method1 = {
{ direction=['in'|'out'], name=ARG0NAME, type=TYPESTR },
...
handler=function(vtab, in0, in1...) return out0, out1... end
}
},
properties = {
Property1 = {
access = ['read'|'readwrite'|'write'],
type = TYPESTR,
get = function(vtab) return VALUE end
set = function(vtab, value)
-- store, e.g. vtab.Propterty1=value
b:emit_properties_changed(PATH, INTF, Property1)
end
}
},
signals = {
Signal1 = {
{ name=ARG0NAME, type=TYPESTR },
...
}
},
}
local b = lsdb.open('user')
b:request_name("well.known.name"))
srv = lsdb.server.new(b, PATH, interface_table)
b:loop()
The vtable
table returned by lsdb.server.new
has the following
fields set: _bus
, _slot
, _path
and _intf
and apart from these
fields can be freely used for storing state such as property values.
Methods handlers and Properties getters/setters can be directly
invoked on the vtable using the vt('METHOD')
, vt:Get('PROPERTY')
and vt:Set('PROPERTY')
respectively.
Additionally, vtable
objects allow emitting declared signals
conveniently via vt:emit('Signal1', arg0, ...)
instead of the longer
bus:emit_signal(path, intf, member, ...)
. See the the API section
for the available methods.
For further information, take a look at the minimal example
examples/tiny-server.lua
and the more extensive one
examples/server.lua
.
function callback(bus, sender, path, interface, member, arg0...)
-- do something
end
slot = bus:match_signal(sender, path, interface, member, callback)
slot = bus:match(match_expr, callback)
These correspond to sd_bus_match_signal(3)
and sd_bus_add_match(3)
respectively.
Note: returning
1
will result in no further callbacks matching the same rule to be called (see manpage for details).
Example: dump all signals on the system bus:
local u = require("utils")
local lsdb = require("lsdbus")
local b = lsdb.open('system')
b:match_signal(nil, nil, nil, nil, function (b,...) u.pp(...) end)
b:loop()
local function callback(bus, usec)
-- usec is planned trigger time
end
evsrc = b:add_periodic(period, accuracy, callback, enabled)
The period
and accuracy
(both in microseconds) correspond to the
same parameters in sd_event_add_time(3)
.
The enabled
defines if periodc event should be enabled after creation.
The default value is true
.
The returned event_src
object supports methods set_enabled(int)
that allows enabling and disabling the event source, and int get_enabled()
,
that returns event's current mode.
See examples/periodic.lua
for an example.
local function callback(bus, signal)
print("received signal:", signal)
end
b:add_signal(lsdb.SIGINT, callback)
The lsdbus module defines the following constants SIGINT
, SIGTERM
,
SIGUSR1
, SIGUSR2
.
Add a callback to be called upon IO activity on the given file
descriptor (see sd_event_add_io(3)
) for details:
Example:
local function callback(bus, fd, revents)
print("activity on fd", fd)
end
b:add_io(fd, lsdb.EPOLLIN|lsdb.EPOLLOUT, callback)
events
are any EPOLLIN
, EPOLLOUT
, EPOLLRDHUP
, EPOLLPRI
or
EPOLLET
.
An example of using this together with the linotify
[3] library can
be found under examples/inotify-io.lua
.
Add a callback to be called upon changes to the given child pid. (see
sd_event_add_child(3)
and waitid(2)
).
local function callback(b, si)
utils.pp(si) -- prints {uid=1000,pid=33236,code=2,status=9}
end
b:add_child(pid, lsdb.WEXITED|lsdb.WSTOPPED|lsdb.WCONTINUED, callback)
status
is either exit code (if code isCLD_EXITED
) or signal otherwisecode
: one oflsdb.CLD_EXITED
,lsdb.CLD_KILLED
,lsdb.CLD_DUMPED
,lsdb.CLD_STOPPED
,lsdb.CLD_TRAPPED
,lsdb.CLD_CONTINUED
(seesi_code
inwaitid(2)
manpage)
D-Bus errors can be returned by raising a Lua error of the following form
error("org.freedesktop.DBus.Error.InvalidArgs|Something is wrong")
-- or using the standard errors lsdbus.errors
error(string.format("%s|%s", lsdbus.error.INVALID_ARGS, "Something is wrong"))
Functions | Description |
---|---|
lsdbus.open(NAME) |
open bus connection |
lsdbus.xml_fromfile(file) |
parse a D-Bus XML file and return as Lua table |
lsdbus.xml_fromstr(str) |
parse a D-Bus XML string and return as Lua table |
lsdbus.find_intf(node, interface |
find and return interface in the introspection table |
lsdbus.tovariant(value) |
encode an arbitray Lua datastructure into a lsdb variant table |
lsdbus.tovariant2(value) |
like above, but just return the value, not the typestr |
Example for tovariant
-- encode a table into a lsdbus variant
> utils.pp(lsdbus.tovariant{a=1,b={foo='hi'}})
{"a{sv}",{a={"x",1},b={"a{sv}",{foo={"s","hi"}}}}}
-- do the message roundtrip via a variant
> utils.pp(b:testmsg('v', lsdbus.tovariant{a=1,b={foo='hi'}}))
{a=1,b={foo="hi"}}
Note: the only limitation of
tovariant
is that for mixed array/dictionary tables, numeric indices get converted to strings, e.g{ 1,2, bar='nope' }
becomes{"1"=1, "2"=2, bar="nope"}
. This is a limitation of D-Bus which doesn't allow dictionaries with heterogeneous keys.
The proxy:SetAV
uses the tovariant functions internally.
Methods | Description |
---|---|
bus:request_name(NAME) |
see sd_bus_request_name(3) |
bus:release_name(NAME) |
see sd_bus_release_name(3) |
open, ready = bus:state() |
see sd_bus_is_open and sd_bus_is_ready |
slot = bus:match_signal(sender, path, intf, member, callback) |
see sd_bus_add_match(3) |
slot = bus:match(match_expr, handler) |
see sd_bus_add_match(3) |
bus:emit_properties_changed(propA, propB...) |
see sd_bus_emit_properties_changed(3) |
bus:emit_signal(path, intf, member, typestr, args...) |
see sd_bus_emit_signal(3) |
evsrc = bus:add_signal(SIGNAL) |
see sd_event_add_signal(3) |
evsrc = bus:add_periodic(period, accuracy, callback) |
see sd_event_add_time_relative(3) |
evsrc = bus:add_io(fd, mask, callback) |
see sd_event_add_io(3) |
evsrc = bus:add_child(pid, options, callback) |
see sd_event_add_child(3) |
bus:loop() |
see sd_event_loop(3) |
bus:run(usec) |
see sd_event_run(3) |
bus:exit_loop() |
see sd_event_exit(3) |
bus:get_fd() |
see sd_event_get_fd(3) |
table = bus:context() |
see sd_bus_message_set_destination(3) etc. |
number = bus:get_method_call_timeout |
see sd_bus_get_method_call_timeout(3) |
bus:set_method_call_timeout |
see sd_bus_set_method_call_timeout(3) |
res = bus:testmsg(typestr, args...) |
test Lua->D-Bus->Lua message roundtrip |
ret, res... = bus:call(dest, path, intf, member, typestr, args...) |
plumbing, prefer lsdbus.proxy |
slot = bus:call_async(callback, dest, path, intf, member, typestr, args...) |
plumbing async method invocation |
slot = bus:add_object_vtable(path, vtab_raw) |
plumbing, use lsdbus.server instead |
Notes:
-
lsdb.open
accepts an optional string parameter to indicate which bus to open:new
(sd_bus_open
)system
(sd_bus_system
)user
(sd_bus_user
)default
(sd_bus_default
)default_system
(sd_bus_default_system
)default_user
(sd_bus_default_user
)
If not given, default is
new'
-
evsrc
(event source) objects are not cleaned up (unref
ed) when garbage collected but set to floating, which means they will live until the event loop is destroyed. To explicitely disable and destroy an event source, call itsunref
method.
Note: upon being collected the
default_*
type bus objects are onlyflush
ed andunref
ed, whereas on non default (new
,system
anduser
) bus objectssd_bus_flush_close_unref
is called.
Note: if there can be other in-process users that use the event loop (i.e. that call
loop
orrun
) in different threads and Lua states, to avoid interference it is advisable to create a new bus and event loop (using thenew
,system
oruser
open variants).
Method | Description |
---|---|
prxy = lsdbus.proxy.new(bus, srv, obj, intf) |
constructor |
prxy(method, arg0, ...) |
call a D-Bus method |
prxy:call(method, arg0, ...) |
same as above, long form |
prxy:HasMethod(method) |
check if prxy has a method with the given name |
prxy:callt(method, ARGTAB) |
call a method with a table of arguments |
prxy:calltt(method, ARGTAB) |
like callt , but returns a result table |
prxy:call_async(method, callback, ARGTAB) |
call a method asynchronously (returns slot) |
prxy:callr(method, arg0, ...) |
raw call, will not unpack variants |
prxy:Get(name) |
get a properties value |
prxy.name |
short form, same as previous |
prxy:Set(name, value) |
set a property |
prxy:SetAV(name, value) |
set a property (auto convert variants) |
prxy.name = value |
short form for setting a property to value |
prxy:GetAll(filter) |
get all properties that match the optional filter |
prxy:SetAll(t) |
set all properties from a table |
prxy:HasProperty(prop) |
check if prxy has a property of the given name |
prxy:Ping |
call the Ping method on org.freedesktop.DBus.Peer |
prxy:error(err, msg) |
error handler, override to customize behavior |
Notes
callt
is a convenience method that can be used for methods with named arguments:b:callt{argA=2, argB="that"}
.GetAll
accepts a filter which can be either- a string [
read
|readwrite
|write
] describing the access mode - a filter function that accepts
(name, value, description)
and returnstrue
orfalse
depending on whether the value shall be included in the result or not.
- a string [
SetAV
("auto variant") will usetovariant
to automatically convert standard Lua types to lsdbus variants. Currently supported area{sv}
,v
,av
anda{iv}
.- see Internals about how
lsdbus.proxy
works.
Method | Description |
---|---|
vt = lsdbus.server.new(bus, path, intf) |
create a new obj with the given path and interface |
vt:call('METHOD', ...) |
locally call the D-Bus method handler |
vt(METHOD, ...) |
same as above |
vt:Get(PROPERTY) |
locally call the get function |
vt:Set(PROPERTY, value) |
locally call the set function |
vt:emit(SIGNAL, args...) |
emit a signal that is defined in the servers interface |
vt:emitPropertiesChanged(prop0, ...) |
emit a PropertiesChanged signal for one or more properties |
vt:emitAllPropertiesChanged(filter) |
emit a PropertiesChanged signal for all properties |
vt:HasMethod(METHOD) |
check if vt has a method |
vt:HasProperty(PROPERTY) |
check if vt has a property |
vt:HasSignal(SIGNAL) |
check if vt has a signal |
vt:get_interface() |
return the original interface |
vt:unref() |
remove the interface and release the resources |
error("dbus.error.name|message") |
return a D-Bus error and message from a callback |
Notes:
- The
emitAllPropertiesChanged(filter)
takes an optional filter function which accepts the property name and property table and returns true or false depending on whether the property shall be included in thePropertiesChanged
signal not. - the vtable slot (
srv.slot
) is garbage collected which will remove the respective dbus interface. Callsrv:unref()
to explicitely remove the interface.
slot
(sd_bus_slot
) objects are returned by match
,
match_signal
, server.new
and call_async
calls.
Method | Description |
---|---|
unref() |
remove slot. calls sd_bus_slot_unref(3) |
The behavior upon garbage collection depends on the slot type:
vtable
:unref
ed, resources freedmatch*
: set to floating (i.e. will continue to exist as long as bus does).call_async
:unref
ed, resources freed
Note: you must hold a reference to a
vtable
slot to prevent is being garbage collected and removed. Typically one just stores a reference to thesrv
object return byserver.new
.
evsrc
(sd_event_source
) objects are returned by bus:add_signal
,
bus:add_periodic
, bus:add_io
and bus:add_child
.
Method | Description |
---|---|
set_enabled(enabled) |
enabled : lsdbus.SD_EVENT_[ON|OFF|ONESHOT] . see sd_event_source_set_enabled(3) |
get_enabled() |
returns lsdbus.SD_EVENT_[ON|OFF|ONESHOT] . see sd_event_source_get_enabled(3) |
unref() |
remove event source. calls sd_event_source_unref(3) |
Note:
evsrc
(event source) objects are not released (unref
ed) when they go out of scope (i.e. are garbage collected) but are set to floating. This means they will live until the event loop is destroyed. To explicitely destroy an event source, call itsunref
method.
Thus, there is no need to store a reference to an evsrc
object
unless you intend to remove it before the program ends.
The fourth parameter of lsdbus.proxy.new
is typically a string
interface name, whose XML is then retrieved using the standard D-Bus
introspection interfaces and converted to a Lua representation using
lsdb.xml_fromstr(xml)
. It is available as proxy.intf
.
However, if introspection is not available, this Lua interface table
can be manually specified and provided directly as the fourth
parameter instead of a string. This way, lsdb.proxy
can be used also
without introspection.
Example
intf = {
name = 'foo.bar.myintf1',
methods = {
Foo = {},
Bar = {
{ name='x', type='s', direction='in' },
{ name='y', type='u', direction='out' }
},
},
properties = {
Flip = { type='s', access='readwrite' },
Flop = { type='a{ss}', access='readwrite' },
Flup = { type='s', access='read' },
}
}
prx = proxy.new(b, 'foo.bar.ick1', '/my/nice/obj', intf)
print(prx)
srv: foo.bar.ick1, obj: /my/nice/obj, intf: foo.bar.myintf1
Methods:
Foo () ->
Bar (s) -> u
Properties:
Flup: s, read
Flip: s, readwrite
Flop: a{ss}, readwrite
Signals:
After installing lsdbus, the tests can be run from the project root as
$ ./scripts/test-runner.sh -r 3 -s
Launching peer test server...
running tests with Lua 5.4, luaunit args -r 3 -s
using bus: default
..................................................................
Ran 66 tests in 0.166 seconds, 66 successes, 0 failures
OK
tests completed with exit code 0
Stopping peer test server with PID 1790834
This also starts the testserver peer service. The environment variable
LUA_VERSION
can be set to specifiy which Lua version to use. All
arguments to the script are passed to luaunit.
To run in dedicated D-Bus session:
$ dbus-run-session -- ./scripts/test-runner.sh
...
LGPLv2. A portion of the lsdbus type conversion is based on code from systemd.
This happens when a bus is created (lsdb.open(...)
) and only used
after more than ~30s. The exact reason is not clear. The workaround is
to immediately use the bus (request_name
, proxy.new
or
server.new
) after opening.
Of course, this can also happen if a non-default bus (open flags
new
, user
and system
) goes out of scope and is collected.
For some reason, after exiting the sd_event loop using exit_loop
,
the bus is disconnected and can't be used anymore. The simple
workaround is to perform any actions requiring the bus before exiting
the loop.
(only API changes)
b:add_periodic
: accepts additional argumentenabled
which defines if periodic event should be enabled or disabled after creation. The default value istrue
==enabled
.- added
proxy:SetAV
(Auto Variant) property set method lsdbus.server
: prefixing of internal fields (.bus
,.path
,.slot
,.intf
) with_
to prevent name collisions with user fields. The low-level vtable is moved to._vt)
.- constructors are now functions: use
lsdbus.server.new
andlsdbus.proxy.new
instead oflsdbus.server:new
andlsdbus.proxy:new
respectivly. lsdb.open()
now defaults tonew
, i.e.sd_bus_open(3)
. This is the safer default, since it will not piggy back onto a potentially existing event loop.- added
bus:get_fd()
(seesd_event_get_fd(3)
) - added
bus:release_name
- vtab slots are now also garbage collected. So make sure to hold
onto the vtab object returned by
server:new
(this itself contains a ref to the slot.slot
) or the interface will dissappear after the next GC cycle. b:add_signal
: takes a numeric signals numbers instead of a string. thelsdbus
module defines constants for supported signals.- additional parameter for callbacks. The callbacks for
match
,match_signal
,call_async
,add_signal
,add_periodic
andadd_io
receive thebus
object as an additional first parameter. This avoids the need to pass the bus (e.g. forb:exit_loop()
via the scope. The server methodhandler
and propertyget
andset
handlers receive thevtable
table returned fromlsdbus.server.new
. Again this avoids the need to pass thebus
object in via the scope and moreover provides a table that can be freely used to store state such as property values.
[1] https://github.com/bluebird75/luaunit.git
[2] https://github.com/kmarkus/uutils.git
[3] https://github.com/hoelzro/linotify