ruby-dbus 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
+