yong-ruby-dbus 0.2.1

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.
@@ -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
+