ruby-dbus-openplacos 0.6.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 (51) hide show
  1. data/COPYING +504 -0
  2. data/NEWS +146 -0
  3. data/README +42 -0
  4. data/Rakefile +54 -0
  5. data/VERSION +1 -0
  6. data/doc/tutorial/index.markdown +480 -0
  7. data/examples/gdbus/gdbus +255 -0
  8. data/examples/gdbus/gdbus.glade +184 -0
  9. data/examples/gdbus/launch.sh +4 -0
  10. data/examples/no-introspect/nm-test.rb +21 -0
  11. data/examples/no-introspect/tracker-test.rb +16 -0
  12. data/examples/rhythmbox/playpause.rb +25 -0
  13. data/examples/service/call_service.rb +25 -0
  14. data/examples/service/service_newapi.rb +51 -0
  15. data/examples/simple/call_introspect.rb +34 -0
  16. data/examples/simple/properties.rb +19 -0
  17. data/examples/utils/listnames.rb +11 -0
  18. data/examples/utils/notify.rb +19 -0
  19. data/lib/dbus/auth.rb +258 -0
  20. data/lib/dbus/bus.rb +947 -0
  21. data/lib/dbus/core_ext/class/attribute.rb +91 -0
  22. data/lib/dbus/core_ext/kernel/singleton_class.rb +14 -0
  23. data/lib/dbus/core_ext/module/remove_method.rb +12 -0
  24. data/lib/dbus/error.rb +44 -0
  25. data/lib/dbus/export.rb +124 -0
  26. data/lib/dbus/introspect.rb +570 -0
  27. data/lib/dbus/marshall.rb +443 -0
  28. data/lib/dbus/matchrule.rb +100 -0
  29. data/lib/dbus/message.rb +310 -0
  30. data/lib/dbus/type.rb +222 -0
  31. data/lib/dbus.rb +83 -0
  32. data/ruby-dbus-openplacos.gemspec +17 -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/property_test.rb +55 -0
  38. data/test/server_robustness_test.rb +72 -0
  39. data/test/server_test.rb +53 -0
  40. data/test/service_newapi.rb +197 -0
  41. data/test/session_bus_test_manual.rb +20 -0
  42. data/test/signal_test.rb +64 -0
  43. data/test/t1 +4 -0
  44. data/test/t2.rb +66 -0
  45. data/test/t3-ticket27.rb +18 -0
  46. data/test/t5-report-dbus-interface.rb +58 -0
  47. data/test/t6-loop.rb +82 -0
  48. data/test/test_env +13 -0
  49. data/test/test_server +39 -0
  50. data/test/variant_test.rb +66 -0
  51. metadata +118 -0
@@ -0,0 +1,570 @@
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
+
426
+ PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties"
427
+
428
+ # Read a property.
429
+ def [](propname)
430
+ self.object[PROPERTY_INTERFACE].Get(self.name, propname)[0]
431
+ end
432
+
433
+ # Write a property.
434
+ def []=(propname, value)
435
+ self.object[PROPERTY_INTERFACE].Set(self.name, propname, value)
436
+ end
437
+
438
+ # Read all properties at once, as a hash.
439
+ def all_properties
440
+ self.object[PROPERTY_INTERFACE].GetAll(self.name)[0]
441
+ end
442
+ end # class ProxyObjectInterface
443
+
444
+ # D-Bus proxy object class
445
+ #
446
+ # Class representing a remote object in an external application.
447
+ # Typically, calling a method on an instance of a ProxyObject sends a message
448
+ # over the bus so that the method is executed remotely on the correctponding
449
+ # object.
450
+ class ProxyObject
451
+ # The subnodes of the object in the tree.
452
+ attr_accessor :subnodes
453
+ # Flag determining whether the object has been introspected.
454
+ attr_accessor :introspected
455
+ # The (remote) destination of the object.
456
+ attr_reader :destination
457
+ # The path to the object.
458
+ attr_reader :path
459
+ # The bus the object is reachable via.
460
+ attr_reader :bus
461
+ # The default interface of the object.
462
+ attr_accessor :default_iface
463
+
464
+ # Creates a new proxy object living on the given _bus_ at destination _dest_
465
+ # on the given _path_.
466
+ def initialize(bus, dest, path)
467
+ @bus, @destination, @path = bus, dest, path
468
+ @interfaces = Hash.new
469
+ @subnodes = Array.new
470
+ end
471
+
472
+ # Returns the interfaces of the object.
473
+ def interfaces
474
+ @interfaces.keys
475
+ end
476
+
477
+ # Retrieves an interface of the proxy object (ProxyObjectInterface instance).
478
+ def [](intfname)
479
+ @interfaces[intfname]
480
+ end
481
+
482
+ # Maps the given interface name _intfname_ to the given interface _intf.
483
+ def []=(intfname, intf)
484
+ @interfaces[intfname] = intf
485
+ end
486
+
487
+ # Introspects the remote object. Allows you to find and select
488
+ # interfaces on the object.
489
+ def introspect
490
+ # Synchronous call here.
491
+ xml = @bus.introspect_data(@destination, @path)
492
+ ProxyObjectFactory.introspect_into(self, xml)
493
+ xml
494
+ end
495
+
496
+ # Returns whether the object has an interface with the given _name_.
497
+ def has_iface?(name)
498
+ raise "Cannot call has_iface? is not introspected" if not @introspected
499
+ @interfaces.member?(name)
500
+ end
501
+
502
+ # Registers a handler, the code block, for a signal with the given _name_.
503
+ def on_signal(name, &block)
504
+ if @default_iface and has_iface?(@default_iface)
505
+ @interfaces[@default_iface].on_signal(@bus, name, &block)
506
+ else
507
+ # TODO improve
508
+ raise NoMethodError
509
+ end
510
+ end
511
+
512
+ ####################################################
513
+ private
514
+
515
+ # Handles all unkown methods, mostly to route method calls to the
516
+ # default interface.
517
+ def method_missing(name, *args)
518
+ if @default_iface and has_iface?(@default_iface)
519
+ begin
520
+ @interfaces[@default_iface].method(name).call(*args)
521
+ rescue NameError => e
522
+ # interesting, foo.method("unknown")
523
+ # raises NameError, not NoMethodError
524
+ match = /undefined method `([^']*)' for class `([^']*)'/.match e.to_s
525
+ raise unless match and match[2] == "DBus::ProxyObjectInterface"
526
+ # BTW e.exception("...") would preserve the class.
527
+ raise NoMethodError,"undefined method `#{match[1]}' for DBus interface `#{@default_iface}' on object `#{@path}'"
528
+ end
529
+ else
530
+ # TODO distinguish:
531
+ # - di not specified
532
+ #TODO
533
+ # - di is specified but not found in introspection data
534
+ raise NoMethodError, "undefined method `#{name}' for DBus interface `#{@default_iface}' on object `#{@path}'"
535
+ end
536
+ end
537
+ end # class ProxyObject
538
+
539
+ # = D-Bus proxy object factory class
540
+ #
541
+ # Class that generates and sets up a proxy object based on introspection data.
542
+ class ProxyObjectFactory
543
+ # Creates a new proxy object factory for the given introspection XML _xml_,
544
+ # _bus_, destination _dest_, and _path_.
545
+ def initialize(xml, bus, dest, path)
546
+ @xml, @bus, @path, @dest = xml, bus, path, dest
547
+ end
548
+
549
+ # Investigates the sub-nodes of the proxy object _po_ based on the
550
+ # introspection XML data _xml_ and sets them up recursively.
551
+ def ProxyObjectFactory.introspect_into(po, xml)
552
+ intfs, po.subnodes = IntrospectXMLParser.new(xml).parse
553
+ intfs.each do |i|
554
+ poi = ProxyObjectInterface.new(po, i.name)
555
+ i.methods.each_value { |m| poi.define(m) }
556
+ i.signals.each_value { |s| poi.define(s) }
557
+ po[i.name] = poi
558
+ end
559
+ po.introspected = true
560
+ end
561
+
562
+ # Generates, sets up and returns the proxy object.
563
+ def build
564
+ po = ProxyObject.new(@bus, @dest, @path)
565
+ ProxyObjectFactory.introspect_into(po, @xml)
566
+ po
567
+ end
568
+ end # class ProxyObjectFactory
569
+ end # module DBus
570
+