ruby-dbus 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +504 -0
- data/NEWS +137 -0
- data/README +53 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/doc/tutorial/index.html +356 -0
- data/doc/tutorial/index.markdown +467 -0
- data/examples/gdbus/gdbus +255 -0
- data/examples/gdbus/gdbus.glade +184 -0
- data/examples/gdbus/launch.sh +4 -0
- data/examples/no-introspect/nm-test.rb +21 -0
- data/examples/no-introspect/tracker-test.rb +16 -0
- data/examples/rhythmbox/playpause.rb +25 -0
- data/examples/service/call_service.rb +25 -0
- data/examples/service/service_newapi.rb +51 -0
- data/examples/simple/call_introspect.rb +34 -0
- data/examples/utils/listnames.rb +11 -0
- data/examples/utils/notify.rb +19 -0
- data/lib/dbus.rb +91 -0
- data/lib/dbus/auth.rb +258 -0
- data/lib/dbus/bus.rb +816 -0
- data/lib/dbus/core_ext/class/attribute.rb +91 -0
- data/lib/dbus/core_ext/kernel/singleton_class.rb +14 -0
- data/lib/dbus/core_ext/module/remove_method.rb +12 -0
- data/lib/dbus/error.rb +44 -0
- data/lib/dbus/export.rb +132 -0
- data/lib/dbus/introspect.rb +553 -0
- data/lib/dbus/marshall.rb +443 -0
- data/lib/dbus/matchrule.rb +100 -0
- data/lib/dbus/message.rb +310 -0
- data/lib/dbus/type.rb +222 -0
- data/ruby-dbus.gemspec +18 -0
- data/test/binding_test.rb +56 -0
- data/test/bus_driver_test.rb +22 -0
- data/test/dbus-launch-simple +35 -0
- data/test/dbus-limited-session.conf +28 -0
- data/test/server_robustness_test.rb +41 -0
- data/test/server_test.rb +53 -0
- data/test/service_newapi.rb +129 -0
- data/test/session_bus_test_manual.rb +20 -0
- data/test/signal_test.rb +64 -0
- data/test/t1 +4 -0
- data/test/t2.rb +66 -0
- data/test/t3-ticket27.rb +18 -0
- data/test/t5-report-dbus-interface.rb +58 -0
- data/test/t6-loop.rb +82 -0
- data/test/test_env +13 -0
- data/test/test_server +39 -0
- data/test/variant_test.rb +66 -0
- metadata +117 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# copied from activesupport/core_ext from Rails, MIT license
|
2
|
+
require 'dbus/core_ext/kernel/singleton_class'
|
3
|
+
require 'dbus/core_ext/module/remove_method'
|
4
|
+
|
5
|
+
class Class
|
6
|
+
# Declare a class-level attribute whose value is inheritable by subclasses.
|
7
|
+
# Subclasses can change their own value and it will not impact parent class.
|
8
|
+
#
|
9
|
+
# class Base
|
10
|
+
# class_attribute :setting
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# class Subclass < Base
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Base.setting = true
|
17
|
+
# Subclass.setting # => true
|
18
|
+
# Subclass.setting = false
|
19
|
+
# Subclass.setting # => false
|
20
|
+
# Base.setting # => true
|
21
|
+
#
|
22
|
+
# In the above case as long as Subclass does not assign a value to setting
|
23
|
+
# by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
|
24
|
+
# would read value assigned to parent class. Once Subclass assigns a value then
|
25
|
+
# the value assigned by Subclass would be returned.
|
26
|
+
#
|
27
|
+
# This matches normal Ruby method inheritance: think of writing an attribute
|
28
|
+
# on a subclass as overriding the reader method. However, you need to be aware
|
29
|
+
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
|
30
|
+
# In such cases, you don't want to do changes in places but use setters:
|
31
|
+
#
|
32
|
+
# Base.setting = []
|
33
|
+
# Base.setting # => []
|
34
|
+
# Subclass.setting # => []
|
35
|
+
#
|
36
|
+
# # Appending in child changes both parent and child because it is the same object:
|
37
|
+
# Subclass.setting << :foo
|
38
|
+
# Base.setting # => [:foo]
|
39
|
+
# Subclass.setting # => [:foo]
|
40
|
+
#
|
41
|
+
# # Use setters to not propagate changes:
|
42
|
+
# Base.setting = []
|
43
|
+
# Subclass.setting += [:foo]
|
44
|
+
# Base.setting # => []
|
45
|
+
# Subclass.setting # => [:foo]
|
46
|
+
#
|
47
|
+
# For convenience, a query method is defined as well:
|
48
|
+
#
|
49
|
+
# Subclass.setting? # => false
|
50
|
+
#
|
51
|
+
# Instances may overwrite the class value in the same way:
|
52
|
+
#
|
53
|
+
# Base.setting = true
|
54
|
+
# object = Base.new
|
55
|
+
# object.setting # => true
|
56
|
+
# object.setting = false
|
57
|
+
# object.setting # => false
|
58
|
+
# Base.setting # => true
|
59
|
+
#
|
60
|
+
# To opt out of the instance writer method, pass :instance_writer => false.
|
61
|
+
#
|
62
|
+
# object.setting = false # => NoMethodError
|
63
|
+
def class_attribute(*attrs)
|
64
|
+
instance_writer = !attrs.last.is_a?(Hash) || attrs.pop[:instance_writer]
|
65
|
+
|
66
|
+
attrs.each do |name|
|
67
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
68
|
+
def self.#{name}() nil end
|
69
|
+
def self.#{name}?() !!#{name} end
|
70
|
+
|
71
|
+
def self.#{name}=(val)
|
72
|
+
singleton_class.class_eval do
|
73
|
+
remove_possible_method(:#{name})
|
74
|
+
define_method(:#{name}) { val }
|
75
|
+
end
|
76
|
+
val
|
77
|
+
end
|
78
|
+
|
79
|
+
def #{name}
|
80
|
+
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
|
81
|
+
end
|
82
|
+
|
83
|
+
def #{name}?
|
84
|
+
!!#{name}
|
85
|
+
end
|
86
|
+
RUBY
|
87
|
+
|
88
|
+
attr_writer name if instance_writer
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# copied from activesupport/core_ext from Rails, MIT license
|
2
|
+
module Kernel
|
3
|
+
# Returns the object's singleton class.
|
4
|
+
def singleton_class
|
5
|
+
class << self
|
6
|
+
self
|
7
|
+
end
|
8
|
+
end unless respond_to?(:singleton_class) # exists in 1.9.2
|
9
|
+
|
10
|
+
# class_eval on an object acts like singleton_class.class_eval.
|
11
|
+
def class_eval(*args, &block)
|
12
|
+
singleton_class.class_eval(*args, &block)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# copied from activesupport/core_ext from Rails, MIT license
|
2
|
+
class Module
|
3
|
+
def remove_possible_method(method)
|
4
|
+
remove_method(method)
|
5
|
+
rescue NameError
|
6
|
+
end
|
7
|
+
|
8
|
+
def redefine_method(method, &block)
|
9
|
+
remove_possible_method(method)
|
10
|
+
define_method(method, &block)
|
11
|
+
end
|
12
|
+
end
|
data/lib/dbus/error.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# error.rb
|
2
|
+
#
|
3
|
+
# This file is part of the ruby-dbus project
|
4
|
+
# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
|
5
|
+
#
|
6
|
+
# This library is free software; you can redistribute it and/or
|
7
|
+
# modify it under the terms of the GNU Lesser General Public
|
8
|
+
# License, version 2.1 as published by the Free Software Foundation.
|
9
|
+
# See the file "COPYING" for the exact licensing terms.
|
10
|
+
|
11
|
+
module DBus
|
12
|
+
# Represents a D-Bus Error, both on the client and server side.
|
13
|
+
class Error < StandardError
|
14
|
+
# error_name. +message+ is inherited from +Exception+
|
15
|
+
attr_reader :name
|
16
|
+
# for received errors, the raw D-Bus message
|
17
|
+
attr_reader :dbus_message
|
18
|
+
|
19
|
+
# If +msg+ is a +DBus::Message+, its contents is used for initialization.
|
20
|
+
# Otherwise, +msg+ is taken as a string and +name+ is used.
|
21
|
+
def initialize(msg, name = "org.freedesktop.DBus.Error.Failed")
|
22
|
+
if msg.is_a? DBus::Message
|
23
|
+
@dbus_message = msg
|
24
|
+
@name = msg.error_name
|
25
|
+
super(msg.params[0]) # or nil
|
26
|
+
if msg.params[1].is_a? Array
|
27
|
+
set_backtrace msg.params[1]
|
28
|
+
end
|
29
|
+
else
|
30
|
+
@name = name
|
31
|
+
super(msg)
|
32
|
+
end
|
33
|
+
# TODO validate error name
|
34
|
+
end
|
35
|
+
end # class Error
|
36
|
+
|
37
|
+
# raise DBus.error, "message"
|
38
|
+
# raise DBus.error("org.example.Error.SeatOccupied"), "Seat #{n} is occupied"
|
39
|
+
def error(name = "org.freedesktop.DBus.Error.Failed")
|
40
|
+
# message will be set by Kernel.raise
|
41
|
+
DBus::Error.new(nil, name)
|
42
|
+
end
|
43
|
+
module_function :error
|
44
|
+
end # module DBus
|
data/lib/dbus/export.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# dbus/introspection.rb - module containing a low-level D-Bus introspection implementation
|
2
|
+
#
|
3
|
+
# This file is part of the ruby-dbus project
|
4
|
+
# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
|
5
|
+
#
|
6
|
+
# This library is free software; you can redistribute it and/or
|
7
|
+
# modify it under the terms of the GNU Lesser General Public
|
8
|
+
# License, version 2.1 as published by the Free Software Foundation.
|
9
|
+
# See the file "COPYING" for the exact licensing terms.
|
10
|
+
|
11
|
+
require 'thread'
|
12
|
+
|
13
|
+
module DBus
|
14
|
+
# Exception raised when an interface cannot be found in an object.
|
15
|
+
class InterfaceNotInObject < Exception
|
16
|
+
end
|
17
|
+
|
18
|
+
# Exception raised when a method cannot be found in an inferface.
|
19
|
+
class MethodNotInInterface < Exception
|
20
|
+
end
|
21
|
+
|
22
|
+
# Method raised when a method returns an invalid return type.
|
23
|
+
class InvalidReturnType < Exception
|
24
|
+
end
|
25
|
+
|
26
|
+
# Exported object type
|
27
|
+
# = Exportable D-Bus object class
|
28
|
+
#
|
29
|
+
# Objects that are going to be exported by a D-Bus service
|
30
|
+
# should inherit from this class.
|
31
|
+
class Object
|
32
|
+
# The path of the object.
|
33
|
+
attr_reader :path
|
34
|
+
# The interfaces that the object supports.
|
35
|
+
class_attribute :intfs
|
36
|
+
# The service that the object is exported by.
|
37
|
+
attr_writer :service
|
38
|
+
|
39
|
+
@@cur_intf = nil
|
40
|
+
@@intfs_mutex = Mutex.new
|
41
|
+
|
42
|
+
# Create a new object with a given _path_.
|
43
|
+
def initialize(path)
|
44
|
+
@path = path
|
45
|
+
@service = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# State that the object implements the given _intf_.
|
49
|
+
def implements(intf)
|
50
|
+
# use a setter
|
51
|
+
self.intfs = (self.intfs || {}).merge({intf.name => intf})
|
52
|
+
end
|
53
|
+
|
54
|
+
# Dispatch a message _msg_.
|
55
|
+
def dispatch(msg)
|
56
|
+
case msg.message_type
|
57
|
+
when Message::METHOD_CALL
|
58
|
+
if not self.intfs[msg.interface]
|
59
|
+
raise InterfaceNotInObject, msg.interface
|
60
|
+
end
|
61
|
+
meth = self.intfs[msg.interface].methods[msg.member.to_sym]
|
62
|
+
raise MethodNotInInterface if not meth
|
63
|
+
methname = Object.make_method_name(msg.interface, msg.member)
|
64
|
+
reply = nil
|
65
|
+
begin
|
66
|
+
retdata = method(methname).call(*msg.params)
|
67
|
+
retdata = [*retdata]
|
68
|
+
|
69
|
+
reply = Message.method_return(msg)
|
70
|
+
meth.rets.zip(retdata).each do |rsig, rdata|
|
71
|
+
reply.add_param(rsig.type, rdata)
|
72
|
+
end
|
73
|
+
rescue => ex
|
74
|
+
reply = ErrorMessage.from_exception(ex).reply_to(msg)
|
75
|
+
end
|
76
|
+
@service.bus.send(reply.marshall)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Select (and create) the interface that the following defined methods
|
81
|
+
# belong to.
|
82
|
+
def self.dbus_interface(s)
|
83
|
+
@@intfs_mutex.synchronize do
|
84
|
+
@@cur_intf = Interface.new(s)
|
85
|
+
self.intfs = (self.intfs || {}).merge({s => @@cur_intf})
|
86
|
+
yield
|
87
|
+
@@cur_intf = nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Dummy undefined interface class.
|
92
|
+
class UndefinedInterface < ScriptError
|
93
|
+
def initialize(sym)
|
94
|
+
super "No interface specified for #{sym}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Defines an exportable method on the object with the given name _sym_,
|
99
|
+
# _prototype_ and the code in a block.
|
100
|
+
def self.dbus_method(sym, protoype = "", &block)
|
101
|
+
raise UndefinedInterface, sym if @@cur_intf.nil?
|
102
|
+
@@cur_intf.define(Method.new(sym.to_s).from_prototype(protoype))
|
103
|
+
define_method(Object.make_method_name(@@cur_intf.name, sym.to_s), &block)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Emits a signal from the object with the given _interface_, signal
|
107
|
+
# _sig_ and arguments _args_.
|
108
|
+
def emit(intf, sig, *args)
|
109
|
+
@service.bus.emit(@service, self, intf, sig, *args)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Defines a signal for the object with a given name _sym_ and _prototype_.
|
113
|
+
def self.dbus_signal(sym, protoype = "")
|
114
|
+
raise UndefinedInterface, sym if @@cur_intf.nil?
|
115
|
+
cur_intf = @@cur_intf
|
116
|
+
signal = Signal.new(sym.to_s).from_prototype(protoype)
|
117
|
+
cur_intf.define(Signal.new(sym.to_s).from_prototype(protoype))
|
118
|
+
define_method(sym.to_s) do |*args|
|
119
|
+
emit(cur_intf, signal, *args)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
####################################################################
|
124
|
+
private
|
125
|
+
|
126
|
+
# Helper method that returns a method name generated from the interface
|
127
|
+
# name _intfname_ and method name _methname_.
|
128
|
+
def self.make_method_name(intfname, methname)
|
129
|
+
"#{intfname}%%#{methname}"
|
130
|
+
end
|
131
|
+
end # class Object
|
132
|
+
end # module DBus
|
@@ -0,0 +1,553 @@
|
|
1
|
+
# dbus/introspection.rb - module containing a low-level D-Bus introspection implementation
|
2
|
+
#
|
3
|
+
# This file is part of the ruby-dbus project
|
4
|
+
# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
|
5
|
+
#
|
6
|
+
# This library is free software; you can redistribute it and/or
|
7
|
+
# modify it under the terms of the GNU Lesser General Public
|
8
|
+
# License, version 2.1 as published by the Free Software Foundation.
|
9
|
+
# See the file "COPYING" for the exact licensing terms.
|
10
|
+
|
11
|
+
require 'rexml/document'
|
12
|
+
|
13
|
+
module DBus
|
14
|
+
# Regular expressions that should match all method names.
|
15
|
+
MethodSignalRE = /^[A-Za-z][A-Za-z0-9_]*$/
|
16
|
+
# Regular expressions that should match all interface names.
|
17
|
+
InterfaceElementRE = /^[A-Za-z][A-Za-z0-9_]*$/
|
18
|
+
|
19
|
+
# Exception raised when an unknown signal is used.
|
20
|
+
class UnknownSignal < Exception
|
21
|
+
end
|
22
|
+
|
23
|
+
# Exception raised when an invalid class definition is encountered.
|
24
|
+
class InvalidClassDefinition < Exception
|
25
|
+
end
|
26
|
+
|
27
|
+
# = D-Bus interface class
|
28
|
+
#
|
29
|
+
# This class is the interface descriptor. In most cases, the Introspect()
|
30
|
+
# method call instanciates and configures this class for us.
|
31
|
+
#
|
32
|
+
# It also is the local definition of interface exported by the program.
|
33
|
+
class Interface
|
34
|
+
# The name of the interface.
|
35
|
+
attr_reader :name
|
36
|
+
# The methods that are part of the interface.
|
37
|
+
attr_reader :methods
|
38
|
+
# The signals that are part of the interface.
|
39
|
+
attr_reader :signals
|
40
|
+
|
41
|
+
# Creates a new interface with a given _name_.
|
42
|
+
def initialize(name)
|
43
|
+
validate_name(name)
|
44
|
+
@name = name
|
45
|
+
@methods, @signals = Hash.new, Hash.new
|
46
|
+
end
|
47
|
+
|
48
|
+
# Validates a service _name_.
|
49
|
+
def validate_name(name)
|
50
|
+
raise InvalidIntrospectionData if name.size > 255
|
51
|
+
raise InvalidIntrospectionData if name =~ /^\./ or name =~ /\.$/
|
52
|
+
raise InvalidIntrospectionData if name =~ /\.\./
|
53
|
+
raise InvalidIntrospectionData if not name =~ /\./
|
54
|
+
name.split(".").each do |element|
|
55
|
+
raise InvalidIntrospectionData if not element =~ InterfaceElementRE
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Helper method for defining a method _m_.
|
60
|
+
def define(m)
|
61
|
+
if m.kind_of?(Method)
|
62
|
+
@methods[m.name.to_sym] = m
|
63
|
+
elsif m.kind_of?(Signal)
|
64
|
+
@signals[m.name.to_sym] = m
|
65
|
+
end
|
66
|
+
end
|
67
|
+
alias :<< :define
|
68
|
+
|
69
|
+
# Defines a method with name _id_ and a given _prototype_ in the
|
70
|
+
# interface.
|
71
|
+
def define_method(id, prototype)
|
72
|
+
m = Method.new(id)
|
73
|
+
m.from_prototype(prototype)
|
74
|
+
define(m)
|
75
|
+
end
|
76
|
+
end # class Interface
|
77
|
+
|
78
|
+
# = A formal parameter has a name and a type
|
79
|
+
class FormalParameter
|
80
|
+
attr_reader :name
|
81
|
+
attr_reader :type
|
82
|
+
|
83
|
+
def initialize(name, type)
|
84
|
+
@name = name
|
85
|
+
@type = type
|
86
|
+
end
|
87
|
+
|
88
|
+
# backward compatibility, deprecated
|
89
|
+
def [](index)
|
90
|
+
case index
|
91
|
+
when 0 then name
|
92
|
+
when 1 then type
|
93
|
+
else nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# = D-Bus interface element class
|
99
|
+
#
|
100
|
+
# This is a generic class for entities that are part of the interface
|
101
|
+
# such as methods and signals.
|
102
|
+
class InterfaceElement
|
103
|
+
# The name of the interface element.
|
104
|
+
attr_reader :name
|
105
|
+
# The parameters of the interface element
|
106
|
+
attr_reader :params
|
107
|
+
|
108
|
+
# Validates element _name_.
|
109
|
+
def validate_name(name)
|
110
|
+
if (not name =~ MethodSignalRE) or (name.size > 255)
|
111
|
+
raise InvalidMethodName, name
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Creates a new element with the given _name_.
|
116
|
+
def initialize(name)
|
117
|
+
validate_name(name.to_s)
|
118
|
+
@name = name
|
119
|
+
@params = Array.new
|
120
|
+
end
|
121
|
+
|
122
|
+
# Adds a formal parameter with _name_ and _signature_
|
123
|
+
# (See also Message#add_param which takes signature+value)
|
124
|
+
def add_fparam(name, signature)
|
125
|
+
@params << FormalParameter.new(name, signature)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Deprecated, for backward compatibility
|
129
|
+
def add_param(name_signature_pair)
|
130
|
+
add_fparam(*name_signature_pair)
|
131
|
+
end
|
132
|
+
end # class InterfaceElement
|
133
|
+
|
134
|
+
# = D-Bus interface method class
|
135
|
+
#
|
136
|
+
# This is a class representing methods that are part of an interface.
|
137
|
+
class Method < InterfaceElement
|
138
|
+
# The list of return values for the method.
|
139
|
+
attr_reader :rets
|
140
|
+
|
141
|
+
# Creates a new method interface element with the given _name_.
|
142
|
+
def initialize(name)
|
143
|
+
super(name)
|
144
|
+
@rets = Array.new
|
145
|
+
end
|
146
|
+
|
147
|
+
# Add a return value _name_ and _signature_.
|
148
|
+
def add_return(name, signature)
|
149
|
+
@rets << FormalParameter.new(name, signature)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Add parameter types by parsing the given _prototype_.
|
153
|
+
def from_prototype(prototype)
|
154
|
+
prototype.split(/, */).each do |arg|
|
155
|
+
arg = arg.split(" ")
|
156
|
+
raise InvalidClassDefinition if arg.size != 2
|
157
|
+
dir, arg = arg
|
158
|
+
if arg =~ /:/
|
159
|
+
arg = arg.split(":")
|
160
|
+
name, sig = arg
|
161
|
+
else
|
162
|
+
sig = arg
|
163
|
+
end
|
164
|
+
case dir
|
165
|
+
when "in"
|
166
|
+
add_fparam(name, sig)
|
167
|
+
when "out"
|
168
|
+
add_return(name, sig)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
self
|
172
|
+
end
|
173
|
+
|
174
|
+
# Return an XML string representation of the method interface elment.
|
175
|
+
def to_xml
|
176
|
+
xml = %{<method name="#{@name}">\n}
|
177
|
+
@params.each do |param|
|
178
|
+
name = param.name ? %{name="#{param.name}" } : ""
|
179
|
+
xml += %{<arg #{name}direction="in" type="#{param.type}"/>\n}
|
180
|
+
end
|
181
|
+
@rets.each do |param|
|
182
|
+
name = param.name ? %{name="#{param.name}" } : ""
|
183
|
+
xml += %{<arg #{name}direction="out" type="#{param.type}"/>\n}
|
184
|
+
end
|
185
|
+
xml += %{</method>\n}
|
186
|
+
xml
|
187
|
+
end
|
188
|
+
end # class Method
|
189
|
+
|
190
|
+
# = D-Bus interface signal class
|
191
|
+
#
|
192
|
+
# This is a class representing signals that are part of an interface.
|
193
|
+
class Signal < InterfaceElement
|
194
|
+
# Add parameter types based on the given _prototype_.
|
195
|
+
def from_prototype(prototype)
|
196
|
+
prototype.split(/, */).each do |arg|
|
197
|
+
if arg =~ /:/
|
198
|
+
arg = arg.split(":")
|
199
|
+
name, sig = arg
|
200
|
+
else
|
201
|
+
sig = arg
|
202
|
+
end
|
203
|
+
add_fparam(name, sig)
|
204
|
+
end
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
# Return an XML string representation of the signal interface elment.
|
209
|
+
def to_xml
|
210
|
+
xml = %{<signal name="#{@name}">\n}
|
211
|
+
@params.each do |param|
|
212
|
+
name = param.name ? %{name="#{param.name}" } : ""
|
213
|
+
xml += %{<arg #{name}type="#{param.type}"/>\n}
|
214
|
+
end
|
215
|
+
xml += %{</signal>\n}
|
216
|
+
xml
|
217
|
+
end
|
218
|
+
end # class Signal
|
219
|
+
|
220
|
+
# = D-Bus introspect XML parser class
|
221
|
+
#
|
222
|
+
# This class parses introspection XML of an object and constructs a tree
|
223
|
+
# of Node, Interface, Method, Signal instances.
|
224
|
+
class IntrospectXMLParser
|
225
|
+
# Creates a new parser for XML data in string _xml_.
|
226
|
+
def initialize(xml)
|
227
|
+
@xml = xml
|
228
|
+
end
|
229
|
+
|
230
|
+
# Recursively parses the subnodes, constructing the tree.
|
231
|
+
def parse_subnodes
|
232
|
+
subnodes = Array.new
|
233
|
+
t = Time.now
|
234
|
+
d = REXML::Document.new(@xml)
|
235
|
+
d.elements.each("node/node") do |e|
|
236
|
+
subnodes << e.attributes["name"]
|
237
|
+
end
|
238
|
+
subnodes
|
239
|
+
end
|
240
|
+
|
241
|
+
# Parses the XML, constructing the tree.
|
242
|
+
def parse
|
243
|
+
ret = Array.new
|
244
|
+
subnodes = Array.new
|
245
|
+
t = Time.now
|
246
|
+
d = REXML::Document.new(@xml)
|
247
|
+
d.elements.each("node/node") do |e|
|
248
|
+
subnodes << e.attributes["name"]
|
249
|
+
end
|
250
|
+
d.elements.each("node/interface") do |e|
|
251
|
+
i = Interface.new(e.attributes["name"])
|
252
|
+
e.elements.each("method") do |me|
|
253
|
+
m = Method.new(me.attributes["name"])
|
254
|
+
parse_methsig(me, m)
|
255
|
+
i << m
|
256
|
+
end
|
257
|
+
e.elements.each("signal") do |se|
|
258
|
+
s = Signal.new(se.attributes["name"])
|
259
|
+
parse_methsig(se, s)
|
260
|
+
i << s
|
261
|
+
end
|
262
|
+
ret << i
|
263
|
+
end
|
264
|
+
d = Time.now - t
|
265
|
+
if d > 2
|
266
|
+
puts "Some XML took more that two secs to parse. Optimize me!" if $DEBUG
|
267
|
+
end
|
268
|
+
[ret, subnodes]
|
269
|
+
end
|
270
|
+
|
271
|
+
######################################################################
|
272
|
+
private
|
273
|
+
|
274
|
+
# Parses a method signature XML element _e_ and initialises
|
275
|
+
# method/signal _m_.
|
276
|
+
def parse_methsig(e, m)
|
277
|
+
e.elements.each("arg") do |ae|
|
278
|
+
name = ae.attributes["name"]
|
279
|
+
dir = ae.attributes["direction"]
|
280
|
+
sig = ae.attributes["type"]
|
281
|
+
if m.is_a?(DBus::Signal)
|
282
|
+
m.add_fparam(name, sig)
|
283
|
+
elsif m.is_a?(DBus::Method)
|
284
|
+
case dir
|
285
|
+
when "in"
|
286
|
+
m.add_fparam(name, sig)
|
287
|
+
when "out"
|
288
|
+
m.add_return(name, sig)
|
289
|
+
end
|
290
|
+
else
|
291
|
+
raise NotImplementedError, dir
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end # class IntrospectXMLParser
|
296
|
+
|
297
|
+
# = D-Bus proxy object interface class
|
298
|
+
#
|
299
|
+
# A class similar to the normal Interface used as a proxy for remote
|
300
|
+
# object interfaces.
|
301
|
+
class ProxyObjectInterface
|
302
|
+
# The proxied methods contained in the interface.
|
303
|
+
attr_accessor :methods
|
304
|
+
# The proxied signals contained in the interface.
|
305
|
+
attr_accessor :signals
|
306
|
+
# The proxy object to which this interface belongs.
|
307
|
+
attr_reader :object
|
308
|
+
# The name of the interface.
|
309
|
+
attr_reader :name
|
310
|
+
|
311
|
+
# Creates a new proxy interface for the given proxy _object_
|
312
|
+
# and the given _name_.
|
313
|
+
def initialize(object, name)
|
314
|
+
@object, @name = object, name
|
315
|
+
@methods, @signals = Hash.new, Hash.new
|
316
|
+
end
|
317
|
+
|
318
|
+
# Returns the string representation of the interface (the name).
|
319
|
+
def to_str
|
320
|
+
@name
|
321
|
+
end
|
322
|
+
|
323
|
+
# Returns the singleton class of the interface.
|
324
|
+
def singleton_class
|
325
|
+
(class << self ; self ; end)
|
326
|
+
end
|
327
|
+
|
328
|
+
# FIXME
|
329
|
+
def check_for_eval(s)
|
330
|
+
raise RuntimeError, "invalid internal data '#{s}'" if not s.to_s =~ /^[A-Za-z0-9_]*$/
|
331
|
+
end
|
332
|
+
|
333
|
+
# FIXME
|
334
|
+
def check_for_quoted_eval(s)
|
335
|
+
raise RuntimeError, "invalid internal data '#{s}'" if not s.to_s =~ /^[^"]+$/
|
336
|
+
end
|
337
|
+
|
338
|
+
# Defines a method on the interface from the Method descriptor _m_.
|
339
|
+
def define_method_from_descriptor(m)
|
340
|
+
check_for_eval(m.name)
|
341
|
+
check_for_quoted_eval(@name)
|
342
|
+
methdef = "def #{m.name}("
|
343
|
+
methdef += (0..(m.params.size - 1)).to_a.collect { |n|
|
344
|
+
"arg#{n}"
|
345
|
+
}.join(", ")
|
346
|
+
methdef += %{)
|
347
|
+
msg = Message.new(Message::METHOD_CALL)
|
348
|
+
msg.path = @object.path
|
349
|
+
msg.interface = "#{@name}"
|
350
|
+
msg.destination = @object.destination
|
351
|
+
msg.member = "#{m.name}"
|
352
|
+
msg.sender = @object.bus.unique_name
|
353
|
+
}
|
354
|
+
idx = 0
|
355
|
+
m.params.each do |fpar|
|
356
|
+
par = fpar.type
|
357
|
+
check_for_quoted_eval(par)
|
358
|
+
|
359
|
+
# This is the signature validity check
|
360
|
+
Type::Parser.new(par).parse
|
361
|
+
|
362
|
+
methdef += %{
|
363
|
+
msg.add_param("#{par}", arg#{idx})
|
364
|
+
}
|
365
|
+
idx += 1
|
366
|
+
end
|
367
|
+
methdef += "
|
368
|
+
ret = nil
|
369
|
+
if block_given?
|
370
|
+
@object.bus.on_return(msg) do |rmsg|
|
371
|
+
if rmsg.is_a?(Error)
|
372
|
+
yield(rmsg)
|
373
|
+
else
|
374
|
+
yield(rmsg, *rmsg.params)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
@object.bus.send(msg.marshall)
|
378
|
+
else
|
379
|
+
@object.bus.send_sync(msg) do |rmsg|
|
380
|
+
if rmsg.is_a?(Error)
|
381
|
+
raise rmsg
|
382
|
+
else
|
383
|
+
ret = rmsg.params
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
ret
|
388
|
+
end
|
389
|
+
"
|
390
|
+
singleton_class.class_eval(methdef)
|
391
|
+
@methods[m.name] = m
|
392
|
+
end
|
393
|
+
|
394
|
+
# Defines a signal from the descriptor _s_.
|
395
|
+
def define_signal_from_descriptor(s)
|
396
|
+
@signals[s.name] = s
|
397
|
+
end
|
398
|
+
|
399
|
+
# Defines a signal or method based on the descriptor _m_.
|
400
|
+
def define(m)
|
401
|
+
if m.kind_of?(Method)
|
402
|
+
define_method_from_descriptor(m)
|
403
|
+
elsif m.kind_of?(Signal)
|
404
|
+
define_signal_from_descriptor(m)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Defines a proxied method on the interface.
|
409
|
+
def define_method(methodname, prototype)
|
410
|
+
m = Method.new(methodname)
|
411
|
+
m.from_prototype(prototype)
|
412
|
+
define(m)
|
413
|
+
end
|
414
|
+
|
415
|
+
# Registers a handler (code block) for a signal with _name_ arriving
|
416
|
+
# over the given _bus_. If no block is given, the signal is unregistered.
|
417
|
+
def on_signal(bus, name, &block)
|
418
|
+
mr = DBus::MatchRule.new.from_signal(self, name)
|
419
|
+
if block.nil?
|
420
|
+
bus.remove_match(mr)
|
421
|
+
else
|
422
|
+
bus.add_match(mr) { |msg| block.call(*msg.params) }
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end # class ProxyObjectInterface
|
426
|
+
|
427
|
+
# D-Bus proxy object class
|
428
|
+
#
|
429
|
+
# Class representing a remote object in an external application.
|
430
|
+
# Typically, calling a method on an instance of a ProxyObject sends a message
|
431
|
+
# over the bus so that the method is executed remotely on the correctponding
|
432
|
+
# object.
|
433
|
+
class ProxyObject
|
434
|
+
# The subnodes of the object in the tree.
|
435
|
+
attr_accessor :subnodes
|
436
|
+
# Flag determining whether the object has been introspected.
|
437
|
+
attr_accessor :introspected
|
438
|
+
# The (remote) destination of the object.
|
439
|
+
attr_reader :destination
|
440
|
+
# The path to the object.
|
441
|
+
attr_reader :path
|
442
|
+
# The bus the object is reachable via.
|
443
|
+
attr_reader :bus
|
444
|
+
# The default interface of the object.
|
445
|
+
attr_accessor :default_iface
|
446
|
+
|
447
|
+
# Creates a new proxy object living on the given _bus_ at destination _dest_
|
448
|
+
# on the given _path_.
|
449
|
+
def initialize(bus, dest, path)
|
450
|
+
@bus, @destination, @path = bus, dest, path
|
451
|
+
@interfaces = Hash.new
|
452
|
+
@subnodes = Array.new
|
453
|
+
end
|
454
|
+
|
455
|
+
# Returns the interfaces of the object.
|
456
|
+
def interfaces
|
457
|
+
@interfaces.keys
|
458
|
+
end
|
459
|
+
|
460
|
+
# Retrieves an interface of the proxy object (ProxyObjectInterface instance).
|
461
|
+
def [](intfname)
|
462
|
+
@interfaces[intfname]
|
463
|
+
end
|
464
|
+
|
465
|
+
# Maps the given interface name _intfname_ to the given interface _intf.
|
466
|
+
def []=(intfname, intf)
|
467
|
+
@interfaces[intfname] = intf
|
468
|
+
end
|
469
|
+
|
470
|
+
# Introspects the remote object. Allows you to find and select
|
471
|
+
# interfaces on the object.
|
472
|
+
def introspect
|
473
|
+
# Synchronous call here.
|
474
|
+
xml = @bus.introspect_data(@destination, @path)
|
475
|
+
ProxyObjectFactory.introspect_into(self, xml)
|
476
|
+
xml
|
477
|
+
end
|
478
|
+
|
479
|
+
# Returns whether the object has an interface with the given _name_.
|
480
|
+
def has_iface?(name)
|
481
|
+
raise "Cannot call has_iface? is not introspected" if not @introspected
|
482
|
+
@interfaces.member?(name)
|
483
|
+
end
|
484
|
+
|
485
|
+
# Registers a handler, the code block, for a signal with the given _name_.
|
486
|
+
def on_signal(name, &block)
|
487
|
+
if @default_iface and has_iface?(@default_iface)
|
488
|
+
@interfaces[@default_iface].on_signal(@bus, name, &block)
|
489
|
+
else
|
490
|
+
# TODO improve
|
491
|
+
raise NoMethodError
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
####################################################
|
496
|
+
private
|
497
|
+
|
498
|
+
# Handles all unkown methods, mostly to route method calls to the
|
499
|
+
# default interface.
|
500
|
+
def method_missing(name, *args)
|
501
|
+
if @default_iface and has_iface?(@default_iface)
|
502
|
+
begin
|
503
|
+
@interfaces[@default_iface].method(name).call(*args)
|
504
|
+
rescue NameError => e
|
505
|
+
# interesting, foo.method("unknown")
|
506
|
+
# raises NameError, not NoMethodError
|
507
|
+
match = /undefined method `([^']*)' for class `([^']*)'/.match e.to_s
|
508
|
+
raise unless match and match[2] == "DBus::ProxyObjectInterface"
|
509
|
+
# BTW e.exception("...") would preserve the class.
|
510
|
+
raise NoMethodError,"undefined method `#{match[1]}' for DBus interface `#{@default_iface}' on object `#{@path}'"
|
511
|
+
end
|
512
|
+
else
|
513
|
+
# TODO distinguish:
|
514
|
+
# - di not specified
|
515
|
+
#TODO
|
516
|
+
# - di is specified but not found in introspection data
|
517
|
+
raise NoMethodError, "undefined method `#{name}' for DBus interface `#{@default_iface}' on object `#{@path}'"
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end # class ProxyObject
|
521
|
+
|
522
|
+
# = D-Bus proxy object factory class
|
523
|
+
#
|
524
|
+
# Class that generates and sets up a proxy object based on introspection data.
|
525
|
+
class ProxyObjectFactory
|
526
|
+
# Creates a new proxy object factory for the given introspection XML _xml_,
|
527
|
+
# _bus_, destination _dest_, and _path_.
|
528
|
+
def initialize(xml, bus, dest, path)
|
529
|
+
@xml, @bus, @path, @dest = xml, bus, path, dest
|
530
|
+
end
|
531
|
+
|
532
|
+
# Investigates the sub-nodes of the proxy object _po_ based on the
|
533
|
+
# introspection XML data _xml_ and sets them up recursively.
|
534
|
+
def ProxyObjectFactory.introspect_into(po, xml)
|
535
|
+
intfs, po.subnodes = IntrospectXMLParser.new(xml).parse
|
536
|
+
intfs.each do |i|
|
537
|
+
poi = ProxyObjectInterface.new(po, i.name)
|
538
|
+
i.methods.each_value { |m| poi.define(m) }
|
539
|
+
i.signals.each_value { |s| poi.define(s) }
|
540
|
+
po[i.name] = poi
|
541
|
+
end
|
542
|
+
po.introspected = true
|
543
|
+
end
|
544
|
+
|
545
|
+
# Generates, sets up and returns the proxy object.
|
546
|
+
def build
|
547
|
+
po = ProxyObject.new(@bus, @dest, @path)
|
548
|
+
ProxyObjectFactory.introspect_into(po, @xml)
|
549
|
+
po
|
550
|
+
end
|
551
|
+
end # class ProxyObjectFactory
|
552
|
+
end # module DBus
|
553
|
+
|