sdague-ruby-dbus 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,123 @@
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
+ attr_reader :intfs
36
+ # The service that the object is exported by.
37
+ attr_writer :service
38
+
39
+ @@intfs = Hash.new
40
+ @@cur_intf = nil
41
+ @@intfs_mutex = Mutex.new
42
+
43
+ # Create a new object with a given _path_.
44
+ def initialize(path)
45
+ @path = path
46
+ @intfs = @@intfs.dup
47
+ @service = nil
48
+ end
49
+
50
+ # State that the object implements the given _intf_.
51
+ def implements(intf)
52
+ @intfs[intf.name] = intf
53
+ end
54
+
55
+ # Dispatch a message _msg_.
56
+ def dispatch(msg)
57
+ case msg.message_type
58
+ when Message::METHOD_CALL
59
+ if not @intfs[msg.interface]
60
+ raise InterfaceNotInObject, msg.interface
61
+ end
62
+ meth = @intfs[msg.interface].methods[msg.member.to_sym]
63
+ raise MethodNotInInterface if not meth
64
+ methname = Object.make_method_name(msg.interface, msg.member)
65
+ retdata = method(methname).call(*msg.params).to_a
66
+
67
+ reply = Message.new.reply_to(msg)
68
+ meth.rets.zip(retdata).each do |rsig, rdata|
69
+ reply.add_param(rsig[1], rdata)
70
+ end
71
+ @service.bus.send(reply.marshall)
72
+ end
73
+ end
74
+
75
+ # Select (and create) the interface that the following defined methods
76
+ # belong to.
77
+ def self.dbus_interface(s)
78
+ @@intfs_mutex.synchronize do
79
+ @@cur_intf = @@intfs[s] = Interface.new(s)
80
+ yield
81
+ @@cur_intf = nil
82
+ end
83
+ end
84
+
85
+ # Dummy undefined interface class.
86
+ class UndefinedInterface
87
+ end
88
+
89
+ # Defines an exportable method on the object with the given name _sym_,
90
+ # _prototype_ and the code in a block.
91
+ def self.dbus_method(sym, protoype = "", &block)
92
+ raise UndefinedInterface if @@cur_intf.nil?
93
+ @@cur_intf.define(Method.new(sym.to_s).from_prototype(protoype))
94
+ define_method(Object.make_method_name(@@cur_intf.name, sym.to_s), &block)
95
+ end
96
+
97
+ # Emits a signal from the object with the given _interface_, signal
98
+ # _sig_ and arguments _args_.
99
+ def emit(intf, sig, *args)
100
+ @service.bus.emit(@service, self, intf, sig, *args)
101
+ end
102
+
103
+ # Defines a signal for the object with a given name _sym_ and _prototype_.
104
+ def self.dbus_signal(sym, protoype = "")
105
+ raise UndefinedInterface if @@cur_intf.nil?
106
+ cur_intf = @@cur_intf
107
+ signal = Signal.new(sym.to_s).from_prototype(protoype)
108
+ cur_intf.define(Signal.new(sym.to_s).from_prototype(protoype))
109
+ define_method(sym.to_s) do |*args|
110
+ emit(cur_intf, signal, *args)
111
+ end
112
+ end
113
+
114
+ ####################################################################
115
+ private
116
+
117
+ # Helper method that returns a method name generated from the interface
118
+ # name _intfname_ and method name _methname_.
119
+ def self.make_method_name(intfname, methname)
120
+ "#{intfname}%%#{methname}"
121
+ end
122
+ end # class Object
123
+ end # module DBus
@@ -0,0 +1,509 @@
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
+ # = D-Bus interface element class
79
+ #
80
+ # This is a generic class for entities that are part of the interface
81
+ # such as methods and signals.
82
+ class InterfaceElement
83
+ # The name of the interface element.
84
+ attr_reader :name
85
+ # The parameters of the interface element
86
+ attr_reader :params
87
+
88
+ # Validates element _name_.
89
+ def validate_name(name)
90
+ if (not name =~ MethodSignalRE) or (name.size > 255)
91
+ raise InvalidMethodName
92
+ end
93
+ end
94
+
95
+ # Creates a new element with the given _name_.
96
+ def initialize(name)
97
+ validate_name(name.to_s)
98
+ @name = name
99
+ @params = Array.new
100
+ end
101
+
102
+ # Adds a parameter _param_.
103
+ def add_param(param)
104
+ @params << param
105
+ end
106
+ end # class InterfaceElement
107
+
108
+ # = D-Bus interface method class
109
+ #
110
+ # This is a class representing methods that are part of an interface.
111
+ class Method < InterfaceElement
112
+ # The list of return values for the method.
113
+ attr_reader :rets
114
+
115
+ # Creates a new method interface element with the given _name_.
116
+ def initialize(name)
117
+ super(name)
118
+ @rets = Array.new
119
+ end
120
+
121
+ # Add a return value _ret_.
122
+ def add_return(ret)
123
+ @rets << ret
124
+ end
125
+
126
+ # Add parameter types by parsing the given _prototype_.
127
+ def from_prototype(prototype)
128
+ prototype.split(/, */).each do |arg|
129
+ arg = arg.split(" ")
130
+ raise InvalidClassDefinition if arg.size != 2
131
+ dir, arg = arg
132
+ if arg =~ /:/
133
+ arg = arg.split(":")
134
+ name, sig = arg
135
+ else
136
+ sig = arg
137
+ end
138
+ case dir
139
+ when "in"
140
+ add_param([name, sig])
141
+ when "out"
142
+ add_return([name, sig])
143
+ end
144
+ end
145
+ self
146
+ end
147
+
148
+ # Return an XML string representation of the method interface elment.
149
+ def to_xml
150
+ xml = %{<method name="#{@name}">\n}
151
+ @params.each do |param|
152
+ name = param[0] ? %{name="#{param[0]}" } : ""
153
+ xml += %{<arg #{name}direction="in" type="#{param[1]}"/>\n}
154
+ end
155
+ @rets.each do |param|
156
+ name = param[0] ? %{name="#{param[0]}" } : ""
157
+ xml += %{<arg #{name}direction="out" type="#{param[1]}"/>\n}
158
+ end
159
+ xml += %{</method>\n}
160
+ xml
161
+ end
162
+ end # class Method
163
+
164
+ # = D-Bus interface signal class
165
+ #
166
+ # This is a class representing signals that are part of an interface.
167
+ class Signal < InterfaceElement
168
+ # Add parameter types based on the given _prototype_.
169
+ def from_prototype(prototype)
170
+ prototype.split(/, */).each do |arg|
171
+ if arg =~ /:/
172
+ arg = arg.split(":")
173
+ name, sig = arg
174
+ else
175
+ sig = arg
176
+ end
177
+ add_param([name, sig])
178
+ end
179
+ self
180
+ end
181
+
182
+ # Return an XML string representation of the signal interface elment.
183
+ def to_xml
184
+ xml = %{<signal name="#{@name}">\n}
185
+ @params.each do |param|
186
+ name = param[0] ? %{name="#{param[0]}" } : ""
187
+ xml += %{<arg #{name}type="#{param[1]}"/>\n}
188
+ end
189
+ xml += %{</signal>\n}
190
+ xml
191
+ end
192
+ end # class Signal
193
+
194
+ # = D-Bus introspect XML parser class
195
+ #
196
+ # This class parses introspection XML of an object and constructs a tree
197
+ # of Node, Interface, Method, Signal instances.
198
+ class IntrospectXMLParser
199
+ # Creates a new parser for XML data in string _xml_.
200
+ def initialize(xml)
201
+ @xml = xml
202
+ end
203
+
204
+ # Recursively parses the subnodes, constructing the tree.
205
+ def parse_subnodes
206
+ subnodes = Array.new
207
+ t = Time.now
208
+ d = REXML::Document.new(@xml)
209
+ d.elements.each("node/node") do |e|
210
+ subnodes << e.attributes["name"]
211
+ end
212
+ subnodes
213
+ end
214
+
215
+ # Parses the XML, constructing the tree.
216
+ def parse
217
+ ret = Array.new
218
+ subnodes = Array.new
219
+ t = Time.now
220
+ d = REXML::Document.new(@xml)
221
+ d.elements.each("node/node") do |e|
222
+ subnodes << e.attributes["name"]
223
+ end
224
+ d.elements.each("node/interface") do |e|
225
+ i = Interface.new(e.attributes["name"])
226
+ e.elements.each("method") do |me|
227
+ m = Method.new(me.attributes["name"])
228
+ parse_methsig(me, m)
229
+ i << m
230
+ end
231
+ e.elements.each("signal") do |se|
232
+ s = Signal.new(se.attributes["name"])
233
+ parse_methsig(se, s)
234
+ i << s
235
+ end
236
+ ret << i
237
+ end
238
+ d = Time.now - t
239
+ if d > 2
240
+ puts "Some XML took more that two secs to parse. Optimize me!" if $DEBUG
241
+ end
242
+ [ret, subnodes]
243
+ end
244
+
245
+ ######################################################################
246
+ private
247
+
248
+ # Parses a method signature XML element _e_ and initialises
249
+ # method/signal _m_.
250
+ def parse_methsig(e, m)
251
+ e.elements.each("arg") do |ae|
252
+ name = ae.attributes["name"]
253
+ dir = ae.attributes["direction"]
254
+ sig = ae.attributes["type"]
255
+ if m.is_a?(DBus::Signal)
256
+ m.add_param([name, sig])
257
+ elsif m.is_a?(DBus::Method)
258
+ case dir
259
+ when "in"
260
+ m.add_param([name, sig])
261
+ when "out"
262
+ m.add_return([name, sig])
263
+ end
264
+ else
265
+ raise NotImplementedError, dir
266
+ end
267
+ end
268
+ end
269
+ end # class IntrospectXMLParser
270
+
271
+ # = D-Bus proxy object interface class
272
+ #
273
+ # A class similar to the normal Interface used as a proxy for remote
274
+ # object interfaces.
275
+ class ProxyObjectInterface
276
+ # The proxied methods contained in the interface.
277
+ attr_accessor :methods
278
+ # The proxied signals contained in the interface.
279
+ attr_accessor :signals
280
+ # The proxy object to which this interface belongs.
281
+ attr_reader :object
282
+ # The name of the interface.
283
+ attr_reader :name
284
+
285
+ # Creates a new proxy interface for the given proxy _object_
286
+ # and the given _name_.
287
+ def initialize(object, name)
288
+ @object, @name = object, name
289
+ @methods, @signals = Hash.new, Hash.new
290
+ end
291
+
292
+ # Returns the string representation of the interface (the name).
293
+ def to_str
294
+ @name
295
+ end
296
+
297
+ # Returns the singleton class of the interface.
298
+ def singleton_class
299
+ (class << self ; self ; end)
300
+ end
301
+
302
+ # FIXME
303
+ def check_for_eval(s)
304
+ raise RuntimeException, "invalid internal data" if not s.to_s =~ /^[A-Za-z0-9_]*$/
305
+ end
306
+
307
+ # FIXME
308
+ def check_for_quoted_eval(s)
309
+ raise RuntimeException, "invalid internal data" if not s.to_s =~ /^[^"]+$/
310
+ end
311
+
312
+ # Defines a method on the interface from the descriptor _m_.
313
+ def define_method_from_descriptor(m)
314
+ check_for_eval(m.name)
315
+ check_for_quoted_eval(@name)
316
+ methdef = "def #{m.name}("
317
+ methdef += (0..(m.params.size - 1)).to_a.collect { |n|
318
+ "arg#{n}"
319
+ }.join(", ")
320
+ methdef += %{)
321
+ msg = Message.new(Message::METHOD_CALL)
322
+ msg.path = @object.path
323
+ msg.interface = "#{@name}"
324
+ msg.destination = @object.destination
325
+ msg.member = "#{m.name}"
326
+ msg.sender = @object.bus.unique_name
327
+ }
328
+ idx = 0
329
+ m.params.each do |npar|
330
+ paramname, par = npar
331
+ check_for_quoted_eval(par)
332
+
333
+ # This is the signature validity check
334
+ Type::Parser.new(par).parse
335
+
336
+ methdef += %{
337
+ msg.add_param("#{par}", arg#{idx})
338
+ }
339
+ idx += 1
340
+ end
341
+ methdef += "
342
+ ret = nil
343
+ if block_given?
344
+ @object.bus.on_return(msg) do |rmsg|
345
+ if rmsg.is_a?(Error)
346
+ yield(rmsg)
347
+ else
348
+ yield(rmsg, *rmsg.params)
349
+ end
350
+ end
351
+ @object.bus.send(msg.marshall)
352
+ else
353
+ @object.bus.send_sync(msg) do |rmsg|
354
+ if rmsg.is_a?(Error)
355
+ raise rmsg
356
+ else
357
+ ret = rmsg.params
358
+ end
359
+ end
360
+ end
361
+ ret
362
+ end
363
+ "
364
+ singleton_class.class_eval(methdef)
365
+ @methods[m.name] = m
366
+ end
367
+
368
+ # Defines a signal from the descriptor _s_.
369
+ def define_signal_from_descriptor(s)
370
+ @signals[s.name] = s
371
+ end
372
+
373
+ # Defines a signal or method based on the descriptor _m_.
374
+ def define(m)
375
+ if m.kind_of?(Method)
376
+ define_method_from_descriptor(m)
377
+ elsif m.kind_of?(Signal)
378
+ define_signal_from_descriptor(m)
379
+ end
380
+ end
381
+
382
+ # Defines a proxied method on the interface.
383
+ def define_method(methodname, prototype)
384
+ m = Method.new(methodname)
385
+ m.from_prototype(prototype)
386
+ define(m)
387
+ end
388
+
389
+ # Registers a handler (code block) for a signal with _name_ arriving
390
+ # over the given _bus_.
391
+ def on_signal(bus, name, &block)
392
+ mr = DBus::MatchRule.new.from_signal(self, name)
393
+ bus.add_match(mr) { |msg| block.call(*msg.params) }
394
+ end
395
+ end # class ProxyObjectInterface
396
+
397
+ # D-Bus proxy object class
398
+ #
399
+ # Class representing a remote object in an external application.
400
+ # Typically, calling a method on an instance of a ProxyObject sends a message
401
+ # over the bus so that the method is executed remotely on the correctponding
402
+ # object.
403
+ class ProxyObject
404
+ # The subnodes of the object in the tree.
405
+ attr_accessor :subnodes
406
+ # Flag determining whether the object has been introspected.
407
+ attr_accessor :introspected
408
+ # The (remote) destination of the object.
409
+ attr_reader :destination
410
+ # The path to the object.
411
+ attr_reader :path
412
+ # The bus the object is reachable via.
413
+ attr_reader :bus
414
+ # The default interface of the object.
415
+ attr_accessor :default_iface
416
+
417
+ # Creates a new proxy object living on the given _bus_ at destination _dest_
418
+ # on the given _path_.
419
+ def initialize(bus, dest, path)
420
+ @bus, @destination, @path = bus, dest, path
421
+ @interfaces = Hash.new
422
+ @subnodes = Array.new
423
+ end
424
+
425
+ # Returns the interfaces of the object.
426
+ def interfaces
427
+ @interfaces.keys
428
+ end
429
+
430
+ # Retrieves an interface of the proxy object (ProxyObjectInterface instance).
431
+ def [](intfname)
432
+ @interfaces[intfname]
433
+ end
434
+
435
+ # Maps the given interface name _intfname_ to the given interface _intf.
436
+ def []=(intfname, intf)
437
+ @interfaces[intfname] = intf
438
+ end
439
+
440
+ # Introspects the remote object. Allows you to find and select
441
+ # interfaces on the object.
442
+ def introspect
443
+ # Synchronous call here.
444
+ xml = @bus.introspect_data(@destination, @path)
445
+ ProxyObjectFactory.introspect_into(self, xml)
446
+ xml
447
+ end
448
+
449
+ # Returns whether the object has an interface with the given _name_.
450
+ def has_iface?(name)
451
+ raise "Cannot call has_iface? is not introspected" if not @introspected
452
+ @interfaces.member?(name)
453
+ end
454
+
455
+ # Registers a handler, the code block, for a signal with the given _name_.
456
+ def on_signal(name, &block)
457
+ if @default_iface and has_iface?(@default_iface)
458
+ @interfaces[@default_iface].on_signal(@bus, name, &block)
459
+ else
460
+ raise NoMethodError
461
+ end
462
+ end
463
+
464
+ ####################################################
465
+ private
466
+
467
+ # Handles all unkown methods, mostly to route method calls to the
468
+ # default interface.
469
+ def method_missing(name, *args)
470
+ if @default_iface and has_iface?(@default_iface)
471
+ @interfaces[@default_iface].method(name).call(*args)
472
+ else
473
+ raise NoMethodError
474
+ end
475
+ end
476
+ end # class ProxyObject
477
+
478
+ # = D-Bus proxy object factory class
479
+ #
480
+ # Class that generates and sets up a proxy object based on introspection data.
481
+ class ProxyObjectFactory
482
+ # Creates a new proxy object factory for the given introspection XML _xml_,
483
+ # _bus_, destination _dest_, and _path_.
484
+ def initialize(xml, bus, dest, path)
485
+ @xml, @bus, @path, @dest = xml, bus, path, dest
486
+ end
487
+
488
+ # Investigates the sub-nodes of the proxy object _po_ based on the
489
+ # introspection XML data _xml_ and sets them up recursively.
490
+ def ProxyObjectFactory.introspect_into(po, xml)
491
+ intfs, po.subnodes = IntrospectXMLParser.new(xml).parse
492
+ intfs.each do |i|
493
+ poi = ProxyObjectInterface.new(po, i.name)
494
+ i.methods.each_value { |m| poi.define(m) }
495
+ i.signals.each_value { |s| poi.define(s) }
496
+ po[i.name] = poi
497
+ end
498
+ po.introspected = true
499
+ end
500
+
501
+ # Generates, sets up and returns the proxy object.
502
+ def build
503
+ po = ProxyObject.new(@bus, @dest, @path)
504
+ ProxyObjectFactory.introspect_into(po, @xml)
505
+ po
506
+ end
507
+ end # class ProxyObjectFactory
508
+ end # module DBus
509
+