Skip to content
/ lsdbus Public
forked from kmarkus/lsdbus

Lua D-Bus interaction made simple

License

Notifications You must be signed in to change notification settings

azadon/lsdbus

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lsdbus

lsdbus is a simple to use D-Bus binding for Lua based on the sd-bus and sd-event APIs.

Table of Contents

Installing

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.

Quickstart

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')

Usage

Bus connection

Before doing anything, it is necessary to connect to a bus:

lsdb = require("lsdbus")
b = lsdb.open()

Type mapping

Testmsg

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 (and testmsgr) 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.

Client API

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.

lsdb.proxy

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.

plumbing API

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."}

Emitting signals

b:emit_signal(PATH, INTERFACE, MEMBER, TYPESTR, ARG0...)

Example

b:emit_signal("/foo", "lsdbus.foo.bar0", "Alarm", "ia{ss}", 999, {x="one", y="two"})

Server API

Event loop

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).

Registering Interfaces: Properties, Methods and Signal

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.

Additionally, vtable objects allow emitting declared signals conveniently via vt:emit('Signal1', arg0, ...) instead of the longer bus:emit_signal(path, intf, member, ...).

For further information, take a look at the minimal example examples/tiny-server.lua and the more extensive one examples/server.lua.

D-Bus signal matching and callbacks

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()

Periodic callbacks

local function callback(bus, usec)
  -- usec is planned trigger time
end

evsrc = b:add_periodic(period, accuracy, callback)

The period and accuracy (both in microseconds) correspond to the same parameters in sd_event_add_time(3).

The returned event_src object supports a method set_enabled(int) that allows enabling and disabling the event source.

See examples/periodic.lua for an example.

Unix Signal callbacks

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.

I/O event callback

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.

Child pid callback

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 is CLD_EXITED) or signal otherwise
  • code: one of lsdb.CLD_EXITED, lsdb.CLD_KILLED, lsdb.CLD_DUMPED, lsdb.CLD_STOPPED, lsdb.CLD_TRAPPED, lsdb.CLD_CONTINUED (see si_code in waitid(2) manpage)

Returning D-Bus errors

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"))

API

Functions

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

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.

Bus connection object

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 (unrefed) 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 its unref method.

Note: upon being collected the default_* type bus objects are only flushed and unrefed, whereas on non default (new, system and user) bus objects sd_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 or run) in different threads and Lua states, to avoid interference it is advisable to create a new bus and event loop (using the new, system or user open variants).

lsdbus.proxy

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: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.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
    1. a string [read|readwrite|write] describing the access mode
    2. a filter function that accepts (name, value, description) and returns true or false depending on whether the value shall be included in the result or not.
  • see Internals about how lsdbus.proxy works.

lsdbus.server

Method Description
srv = lsdbus.server.new(bus, path, intf) create a new obj with the given path and interface
emit(signal, args...) emit a signal that is defined in the servers interface
emitPropertiesChanged(prop0, ...) emit a PropertiesChanged signal for one or more properties
emitAllPropertiesChanged(filter) emit a PropertiesChanged signal for all properties
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 the PropertiesChanged signal not.
  • the vtable slot (srv.slot) is garbage collected which will remove the respective dbus interface. Call srv:unref() to explicitely remove the interface.

slots

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: unrefed, resources freed
  • match*: set to floating (i.e. will continue to exist as long as bus does).
  • call_async: unrefed, 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 the srv object return by server.new.

event sources

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)
unref() remove event source. calls sd_event_source_unref(3)

Note: evsrc (event source) objects are not released (unrefed) 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 its unref method.

Thus, there is no need to store a reference to an evsrc object unless you intend to remove it before the program ends.

Internals

Introspection

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:

License

LGPLv2. A portion of the lsdbus type conversion is based on code from systemd.

FAQ

Error System.Error.ENOTCONN: Transport endpoint is not connected

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.

System.Error.ENOTCONN after exiting loop

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.

ChangeLog

  • lsdb.open() now defaults to new, 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() (see sd_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.

References

[1] https://github.com/bluebird75/luaunit.git
[2] https://github.com/kmarkus/uutils.git
[3] https://github.com/hoelzro/linotify

About

Lua D-Bus interaction made simple

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C 54.0%
  • Lua 44.6%
  • CMake 1.4%