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.
Files changed (50) hide show
  1. data/COPYING +504 -0
  2. data/NEWS +137 -0
  3. data/README +53 -0
  4. data/Rakefile +54 -0
  5. data/VERSION +1 -0
  6. data/doc/tutorial/index.html +356 -0
  7. data/doc/tutorial/index.markdown +467 -0
  8. data/examples/gdbus/gdbus +255 -0
  9. data/examples/gdbus/gdbus.glade +184 -0
  10. data/examples/gdbus/launch.sh +4 -0
  11. data/examples/no-introspect/nm-test.rb +21 -0
  12. data/examples/no-introspect/tracker-test.rb +16 -0
  13. data/examples/rhythmbox/playpause.rb +25 -0
  14. data/examples/service/call_service.rb +25 -0
  15. data/examples/service/service_newapi.rb +51 -0
  16. data/examples/simple/call_introspect.rb +34 -0
  17. data/examples/utils/listnames.rb +11 -0
  18. data/examples/utils/notify.rb +19 -0
  19. data/lib/dbus.rb +91 -0
  20. data/lib/dbus/auth.rb +258 -0
  21. data/lib/dbus/bus.rb +816 -0
  22. data/lib/dbus/core_ext/class/attribute.rb +91 -0
  23. data/lib/dbus/core_ext/kernel/singleton_class.rb +14 -0
  24. data/lib/dbus/core_ext/module/remove_method.rb +12 -0
  25. data/lib/dbus/error.rb +44 -0
  26. data/lib/dbus/export.rb +132 -0
  27. data/lib/dbus/introspect.rb +553 -0
  28. data/lib/dbus/marshall.rb +443 -0
  29. data/lib/dbus/matchrule.rb +100 -0
  30. data/lib/dbus/message.rb +310 -0
  31. data/lib/dbus/type.rb +222 -0
  32. data/ruby-dbus.gemspec +18 -0
  33. data/test/binding_test.rb +56 -0
  34. data/test/bus_driver_test.rb +22 -0
  35. data/test/dbus-launch-simple +35 -0
  36. data/test/dbus-limited-session.conf +28 -0
  37. data/test/server_robustness_test.rb +41 -0
  38. data/test/server_test.rb +53 -0
  39. data/test/service_newapi.rb +129 -0
  40. data/test/session_bus_test_manual.rb +20 -0
  41. data/test/signal_test.rb +64 -0
  42. data/test/t1 +4 -0
  43. data/test/t2.rb +66 -0
  44. data/test/t3-ticket27.rb +18 -0
  45. data/test/t5-report-dbus-interface.rb +58 -0
  46. data/test/t6-loop.rb +82 -0
  47. data/test/test_env +13 -0
  48. data/test/test_server +39 -0
  49. data/test/variant_test.rb +66 -0
  50. 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
@@ -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
@@ -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
+