# frozen_string_literal: true
# dbus.rb - Module containing the low-level D-Bus implementation
#
# This file is part of the ruby-dbus project
# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
#
# This library is free software; you caan redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License, version 2.1 as published by the Free Software Foundation.
# See the file "COPYING" for the exact licensing terms.
require "socket"
require "singleton"
# = D-Bus main module
#
# Module containing all the D-Bus modules and classes.
module DBus
# This represents a remote service. It should not be instantiated directly
# Use {Connection#service}
class Service
# The service name.
attr_reader :name
# The bus the service is running on.
attr_reader :bus
# The service root (FIXME).
attr_reader :root
# Create a new service with a given _name_ on a given _bus_.
def initialize(name, bus)
@name = BusName.new(name)
@bus = bus
@root = Node.new("/")
end
# Determine whether the service name already exists.
def exists?
bus.proxy.ListNames[0].member?(@name)
end
# Perform an introspection on all the objects on the service
# (starting recursively from the root).
def introspect
raise NotImplementedError if block_given?
rec_introspect(@root, "/")
self
end
# Retrieves an object at the given _path_.
# @param path [ObjectPath]
# @return [ProxyObject]
def [](path)
object(path, api: ApiOptions::A1)
end
# Retrieves an object at the given _path_
# whose methods always return an array.
# @param path [ObjectPath]
# @param api [ApiOptions]
# @return [ProxyObject]
def object(path, api: ApiOptions::A0)
node = get_node(path, create: true)
if node.object.nil? || node.object.api != api
node.object = ProxyObject.new(
@bus, @name, path,
api: api
)
end
node.object
end
# Export an object
# @param obj [DBus::Object]
def export(obj)
obj.service = self
get_node(obj.path, create: true).object = obj
end
# Undo exporting an object _obj_.
# Raises ArgumentError if it is not a DBus::Object.
# Returns the object, or false if _obj_ was not exported.
# @param obj [DBus::Object]
def unexport(obj)
raise ArgumentError, "DBus::Service#unexport() expects a DBus::Object argument" unless obj.is_a?(DBus::Object)
return false unless obj.path
last_path_separator_idx = obj.path.rindex("/")
parent_path = obj.path[1..last_path_separator_idx - 1]
node_name = obj.path[last_path_separator_idx + 1..-1]
parent_node = get_node(parent_path, create: false)
return false unless parent_node
obj.service = nil
parent_node.delete(node_name).object
end
# Get the object node corresponding to the given *path*.
# @param path [ObjectPath]
# @param create [Boolean] if true, the the {Node}s in the path are created
# if they do not already exist.
# @return [Node,nil]
def get_node(path, create: false)
n = @root
path.sub(%r{^/}, "").split("/").each do |elem|
if !(n[elem])
return nil if !create
n[elem] = Node.new(elem)
end
n = n[elem]
end
if n.nil?
DBus.logger.debug "Warning, unknown object #{path}"
end
n
end
#########
private
#########
# Perform a recursive retrospection on the given current _node_
# on the given _path_.
def rec_introspect(node, path)
xml = bus.introspect_data(@name, path)
intfs, subnodes = IntrospectXMLParser.new(xml).parse
subnodes.each do |nodename|
subnode = node[nodename] = Node.new(nodename)
subpath = if path == "/"
"/#{nodename}"
else
"#{path}/#{nodename}"
end
rec_introspect(subnode, subpath)
end
return if intfs.empty?
node.object = ProxyObjectFactory.new(xml, @bus, @name, path).build
end
end
# = Object path node class
#
# Class representing a node on an object path.
class Node < Hash
# @return [DBus::Object,DBus::ProxyObject,nil]
# The D-Bus object contained by the node.
attr_accessor :object
# The name of the node.
# @return [String] the last component of its object path, or "/"
attr_reader :name
# Create a new node with a given _name_.
def initialize(name)
super()
@name = name
@object = nil
end
# Return an XML string representation of the node.
# It is shallow, not recursing into subnodes
# @param node_opath [String]
def to_xml(node_opath)
xml = '
'
xml += "\n"
each_key do |k|
xml += " \n"
end
@object&.intfs&.each_value do |v|
xml += v.to_xml
end
xml += ""
xml
end
# Return inspect information of the node.
def inspect
# Need something here
""
end
# Return instance inspect information, used by Node#inspect.
def sub_inspect
s = ""
if !@object.nil?
s += format("%x ", @object.object_id)
end
contents_sub_inspect = keys
.map { |k| "#{k} => #{self[k].sub_inspect}" }
.join(",")
"#{s}{#{contents_sub_inspect}}"
end
end
# FIXME: rename Connection to Bus?
# D-Bus main connection class
#
# Main class that maintains a connection to a bus and can handle incoming
# and outgoing messages.
class Connection
# The unique name (by specification) of the message.
attr_reader :unique_name
# pop and push messages here
attr_reader :message_queue
# Create a new connection to the bus for a given connect _path_. _path_
# format is described in the D-Bus specification:
# http://dbus.freedesktop.org/doc/dbus-specification.html#addresses
# and is something like:
# "transport1:key1=value1,key2=value2;transport2:key1=value1,key2=value2"
# e.g. "unix:path=/tmp/dbus-test" or "tcp:host=localhost,port=2687"
def initialize(path)
@message_queue = MessageQueue.new(path)
@unique_name = nil
# @return [Hash{Integer => Proc}]
# key: message serial
# value: block to be run when the reply to that message is received
@method_call_replies = {}
# @return [Hash{Integer => Message}]
# for debugging only: messages for which a reply was not received yet;
# key == value.serial
@method_call_msgs = {}
@signal_matchrules = {}
@proxy = nil
@object_root = Node.new("/")
end
# Dispatch all messages that are available in the queue,
# but do not block on the queue.
# Called by a main loop when something is available in the queue
def dispatch_message_queue
while (msg = @message_queue.pop(blocking: false)) # FIXME: EOFError
process(msg)
end
end
# Tell a bus to register itself on the glib main loop
def glibize
require "glib2"
# Circumvent a ruby-glib bug
@channels ||= []
gio = GLib::IOChannel.new(@message_queue.socket.fileno)
@channels << gio
gio.add_watch(GLib::IOChannel::IN) do |_c, _ch|
dispatch_message_queue
true
end
end
# FIXME: describe the following names, flags and constants.
# See DBus spec for definition
NAME_FLAG_ALLOW_REPLACEMENT = 0x1
NAME_FLAG_REPLACE_EXISTING = 0x2
NAME_FLAG_DO_NOT_QUEUE = 0x4
REQUEST_NAME_REPLY_PRIMARY_OWNER = 0x1
REQUEST_NAME_REPLY_IN_QUEUE = 0x2
REQUEST_NAME_REPLY_EXISTS = 0x3
REQUEST_NAME_REPLY_ALREADY_OWNER = 0x4
DBUSXMLINTRO = '
'
# This apostroph is for syntax highlighting editors confused by above xml: "
# @api private
# Send a _message_.
# If _reply_handler_ is not given, wait for the reply
# and return the reply, or raise the error.
# If _reply_handler_ is given, it will be called when the reply
# eventually arrives, with the reply message as the 1st param
# and its params following
def send_sync_or_async(message, &reply_handler)
ret = nil
if reply_handler.nil?
send_sync(message) do |rmsg|
raise rmsg if rmsg.is_a?(Error)
ret = rmsg.params
end
else
on_return(message) do |rmsg|
if rmsg.is_a?(Error)
reply_handler.call(rmsg)
else
reply_handler.call(rmsg, * rmsg.params)
end
end
@message_queue.push(message)
end
ret
end
# @api private
def introspect_data(dest, path, &reply_handler)
m = DBus::Message.new(DBus::Message::METHOD_CALL)
m.path = path
m.interface = "org.freedesktop.DBus.Introspectable"
m.destination = dest
m.member = "Introspect"
m.sender = unique_name
if reply_handler.nil?
send_sync_or_async(m).first
else
send_sync_or_async(m) do |*args|
# TODO: test async introspection, is it used at all?
args.shift # forget the message, pass only the text
reply_handler.call(*args)
nil
end
end
end
# @api private
# Issues a call to the org.freedesktop.DBus.Introspectable.Introspect method
# _dest_ is the service and _path_ the object path you want to introspect
# If a code block is given, the introspect call in asynchronous. If not
# data is returned
#
# FIXME: link to ProxyObject data definition
# The returned object is a ProxyObject that has methods you can call to
# issue somme METHOD_CALL messages, and to setup to receive METHOD_RETURN
def introspect(dest, path)
if !block_given?
# introspect in synchronous !
data = introspect_data(dest, path)
pof = DBus::ProxyObjectFactory.new(data, self, dest, path)
pof.build
else
introspect_data(dest, path) do |async_data|
yield(DBus::ProxyObjectFactory.new(async_data, self, dest, path).build)
end
end
end
# Exception raised when a service name is requested that is not available.
class NameRequestError < Exception
end
# Attempt to request a service _name_.
#
# FIXME, NameRequestError cannot really be rescued as it will be raised
# when dispatching a later call. Rework the API to better match the spec.
# @return [Service]
def request_service(name)
# Use RequestName, but asynchronously!
# A synchronous call would not work with service activation, where
# method calls to be serviced arrive before the reply for RequestName
# (Ticket#29).
proxy.RequestName(name, NAME_FLAG_REPLACE_EXISTING) do |rmsg, r|
# check and report errors first
raise rmsg if rmsg.is_a?(Error)
raise NameRequestError unless r == REQUEST_NAME_REPLY_PRIMARY_OWNER
end
@service = Service.new(name, self)
@service
end
# Set up a ProxyObject for the bus itself, since the bus is introspectable.
# @return [ProxyObject] that always returns an array
# ({DBus::ApiOptions#proxy_method_returns_array})
# Returns the object.
def proxy
if @proxy.nil?
path = "/org/freedesktop/DBus"
dest = "org.freedesktop.DBus"
pof = DBus::ProxyObjectFactory.new(
DBUSXMLINTRO, self, dest, path,
api: ApiOptions::A0
)
@proxy = pof.build["org.freedesktop.DBus"]
end
@proxy
end
# @api private
# Wait for a message to arrive. Return it once it is available.
def wait_for_message
@message_queue.pop # FIXME: EOFError
end
# @api private
# Send a message _msg_ on to the bus. This is done synchronously, thus
# the call will block until a reply message arrives.
# @param msg [Message]
# @param retc [Proc] the reply handler
# @yieldparam rmsg [MethodReturnMessage] the reply
# @yieldreturn [Array