ruby-dbus 0.16.0 → 0.18.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS.md +160 -0
  3. data/README.md +3 -5
  4. data/Rakefile +18 -8
  5. data/VERSION +1 -1
  6. data/doc/Reference.md +106 -7
  7. data/examples/doc/_extract_examples +7 -0
  8. data/examples/gdbus/gdbus +31 -24
  9. data/examples/no-introspect/nm-test.rb +2 -0
  10. data/examples/no-introspect/tracker-test.rb +3 -1
  11. data/examples/rhythmbox/playpause.rb +2 -1
  12. data/examples/service/call_service.rb +2 -1
  13. data/examples/service/complex-property.rb +21 -0
  14. data/examples/service/service_newapi.rb +1 -1
  15. data/examples/simple/call_introspect.rb +1 -0
  16. data/examples/simple/get_id.rb +2 -1
  17. data/examples/simple/properties.rb +2 -0
  18. data/examples/utils/listnames.rb +1 -0
  19. data/examples/utils/notify.rb +1 -0
  20. data/lib/dbus/api_options.rb +9 -0
  21. data/lib/dbus/auth.rb +20 -15
  22. data/lib/dbus/bus.rb +123 -75
  23. data/lib/dbus/bus_name.rb +12 -8
  24. data/lib/dbus/core_ext/class/attribute.rb +1 -1
  25. data/lib/dbus/data.rb +821 -0
  26. data/lib/dbus/emits_changed_signal.rb +83 -0
  27. data/lib/dbus/error.rb +4 -2
  28. data/lib/dbus/introspect.rb +132 -31
  29. data/lib/dbus/logger.rb +3 -1
  30. data/lib/dbus/marshall.rb +247 -296
  31. data/lib/dbus/matchrule.rb +16 -16
  32. data/lib/dbus/message.rb +44 -37
  33. data/lib/dbus/message_queue.rb +16 -10
  34. data/lib/dbus/object.rb +358 -24
  35. data/lib/dbus/object_path.rb +11 -6
  36. data/lib/dbus/proxy_object.rb +22 -1
  37. data/lib/dbus/proxy_object_factory.rb +13 -7
  38. data/lib/dbus/proxy_object_interface.rb +63 -30
  39. data/lib/dbus/raw_message.rb +91 -0
  40. data/lib/dbus/type.rb +318 -86
  41. data/lib/dbus/xml.rb +32 -17
  42. data/lib/dbus.rb +14 -7
  43. data/ruby-dbus.gemspec +7 -3
  44. data/spec/async_spec.rb +2 -0
  45. data/spec/binding_spec.rb +2 -0
  46. data/spec/bus_and_xml_backend_spec.rb +2 -0
  47. data/spec/bus_driver_spec.rb +2 -0
  48. data/spec/bus_name_spec.rb +3 -1
  49. data/spec/bus_spec.rb +2 -0
  50. data/spec/byte_array_spec.rb +2 -0
  51. data/spec/client_robustness_spec.rb +4 -2
  52. data/spec/data/marshall.yaml +1667 -0
  53. data/spec/data_spec.rb +673 -0
  54. data/spec/emits_changed_signal_spec.rb +58 -0
  55. data/spec/err_msg_spec.rb +2 -0
  56. data/spec/introspect_xml_parser_spec.rb +2 -0
  57. data/spec/introspection_spec.rb +2 -0
  58. data/spec/main_loop_spec.rb +3 -1
  59. data/spec/node_spec.rb +23 -0
  60. data/spec/object_path_spec.rb +3 -0
  61. data/spec/object_spec.rb +138 -0
  62. data/spec/packet_marshaller_spec.rb +41 -0
  63. data/spec/packet_unmarshaller_spec.rb +248 -0
  64. data/spec/property_spec.rb +192 -5
  65. data/spec/proxy_object_spec.rb +2 -0
  66. data/spec/server_robustness_spec.rb +2 -0
  67. data/spec/server_spec.rb +2 -0
  68. data/spec/service_newapi.rb +70 -70
  69. data/spec/session_bus_spec.rb +3 -1
  70. data/spec/session_bus_spec_manual.rb +2 -0
  71. data/spec/signal_spec.rb +5 -3
  72. data/spec/spec_helper.rb +37 -9
  73. data/spec/thread_safety_spec.rb +2 -0
  74. data/spec/tools/dbus-limited-session.conf +4 -0
  75. data/spec/type_spec.rb +214 -6
  76. data/spec/value_spec.rb +16 -1
  77. data/spec/variant_spec.rb +4 -2
  78. data/spec/zzz_quit_spec.rb +16 -0
  79. metadata +34 -8
data/lib/dbus/object.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file is part of the ruby-dbus project
2
4
  # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
3
5
  #
@@ -6,10 +8,11 @@
6
8
  # License, version 2.1 as published by the Free Software Foundation.
7
9
  # See the file "COPYING" for the exact licensing terms.
8
10
 
9
- require "thread"
10
- require "dbus/core_ext/class/attribute"
11
+ require_relative "core_ext/class/attribute"
11
12
 
12
13
  module DBus
14
+ PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties"
15
+
13
16
  # Exported object type
14
17
  # = Exportable D-Bus object class
15
18
  #
@@ -18,6 +21,7 @@ module DBus
18
21
  class Object
19
22
  # The path of the object.
20
23
  attr_reader :path
24
+
21
25
  # The interfaces that the object supports. Hash: String => Interface
22
26
  my_class_attribute :intfs
23
27
  self.intfs = {}
@@ -41,11 +45,13 @@ module DBus
41
45
  when Message::METHOD_CALL
42
46
  reply = nil
43
47
  begin
44
- if !intfs[msg.interface]
48
+ iface = intfs[msg.interface]
49
+ if !iface
45
50
  raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
46
51
  "Interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
47
52
  end
48
- meth = intfs[msg.interface].methods[msg.member.to_sym]
53
+ member_sym = msg.member.to_sym
54
+ meth = iface.methods[member_sym]
49
55
  if !meth
50
56
  raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
51
57
  "Method \"#{msg.member}\" on interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
@@ -55,11 +61,12 @@ module DBus
55
61
  retdata = [*retdata]
56
62
 
57
63
  reply = Message.method_return(msg)
58
- meth.rets.zip(retdata).each do |rsig, rdata|
59
- reply.add_param(rsig.type, rdata)
64
+ rsigs = meth.rets.map(&:type)
65
+ rsigs.zip(retdata).each do |rsig, rdata|
66
+ reply.add_param(rsig, rdata)
60
67
  end
61
- rescue => ex
62
- dbus_msg_exc = msg.annotate_exception(ex)
68
+ rescue StandardError => e
69
+ dbus_msg_exc = msg.annotate_exception(e)
63
70
  reply = ErrorMessage.from_exception(dbus_msg_exc).reply_to(msg)
64
71
  end
65
72
  @service.bus.message_queue.push(reply)
@@ -68,62 +75,389 @@ module DBus
68
75
 
69
76
  # Select (and create) the interface that the following defined methods
70
77
  # belong to.
71
- def self.dbus_interface(s)
78
+ # @param name [String] interface name like "org.example.ManagerManager"
79
+ # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface
80
+ def self.dbus_interface(name)
72
81
  @@intfs_mutex.synchronize do
73
- @@cur_intf = intfs[s]
82
+ @@cur_intf = intfs[name]
74
83
  if !@@cur_intf
75
- @@cur_intf = Interface.new(s)
84
+ @@cur_intf = Interface.new(name) # validates the name
76
85
  # As this is a mutable class_attr, we cannot use
77
- # self.intfs[s] = @@cur_intf # Hash#[]=
86
+ # self.intfs[name] = @@cur_intf # Hash#[]=
78
87
  # as that would modify parent class attr in place.
79
88
  # Using the setter lets a subclass have the new value
80
89
  # while the superclass keeps the old one.
81
- self.intfs = intfs.merge(s => @@cur_intf)
90
+ self.intfs = intfs.merge(name => @@cur_intf)
82
91
  end
83
92
  yield
84
93
  @@cur_intf = nil
85
94
  end
86
95
  end
87
96
 
88
- # Dummy undefined interface class.
89
- class UndefinedInterface < ScriptError
97
+ # Forgetting to declare the interface for a method/signal/property
98
+ # is a ScriptError.
99
+ class UndefinedInterface < ScriptError # rubocop:disable Lint/InheritException
90
100
  def initialize(sym)
91
- super "No interface specified for #{sym}"
101
+ super "No interface specified for #{sym}. Enclose it in dbus_interface."
102
+ end
103
+ end
104
+
105
+ # Declare the behavior of PropertiesChanged signal,
106
+ # common for all properties in this interface
107
+ # (individual properties may override it)
108
+ # @example
109
+ # self.emits_changed_signal = :invalidates
110
+ # @param [true,false,:const,:invalidates] value
111
+ def self.emits_changed_signal=(value)
112
+ raise UndefinedInterface, :emits_changed_signal if @@cur_intf.nil?
113
+
114
+ @@cur_intf.emits_changed_signal = EmitsChangedSignal.new(value)
115
+ end
116
+
117
+ # A read-write property accessing an instance variable.
118
+ # A combination of `attr_accessor` and {.dbus_accessor}.
119
+ #
120
+ # PropertiesChanged signal will be emitted whenever `foo_bar=` is used
121
+ # but not when @foo_bar is written directly.
122
+ #
123
+ # @param ruby_name [Symbol] :foo_bar is exposed as FooBar;
124
+ # use dbus_name to override
125
+ # @param type a signature like "s" or "a(uus)" or Type::STRING
126
+ # @param dbus_name [String] if not given it is made
127
+ # by CamelCasing the ruby_name. foo_bar becomes FooBar
128
+ # to convert the Ruby convention to the DBus convention.
129
+ # @param emits_changed_signal [true,false,:const,:invalidates]
130
+ # see {EmitsChangedSignal}; if unspecified, ask the interface.
131
+ # @return [void]
132
+ def self.dbus_attr_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
133
+ attr_accessor(ruby_name)
134
+
135
+ dbus_accessor(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
136
+ end
137
+
138
+ # A read-only property accessing an instance variable.
139
+ # A combination of `attr_reader` and {.dbus_reader}.
140
+ #
141
+ # Whenever the property value gets changed from "inside" the object,
142
+ # you should emit the `PropertiesChanged` signal by calling
143
+ # {#dbus_properties_changed}.
144
+ #
145
+ # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
146
+ #
147
+ # or, omitting the value in the signal,
148
+ #
149
+ # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
150
+ #
151
+ # @param (see .dbus_attr_accessor)
152
+ # @return (see .dbus_attr_accessor)
153
+ def self.dbus_attr_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
154
+ attr_reader(ruby_name)
155
+
156
+ dbus_reader(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
157
+ end
158
+
159
+ # A write-only property accessing an instance variable.
160
+ # A combination of `attr_writer` and {.dbus_writer}.
161
+ #
162
+ # @param (see .dbus_attr_accessor)
163
+ # @return (see .dbus_attr_accessor)
164
+ def self.dbus_attr_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
165
+ attr_writer(ruby_name)
166
+
167
+ dbus_writer(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
168
+ end
169
+
170
+ # A read-write property using a pair of reader/writer methods
171
+ # (which must already exist).
172
+ # (To directly access an instance variable, use {.dbus_attr_accessor} instead)
173
+ #
174
+ # Uses {.dbus_watcher} to set up the PropertiesChanged signal.
175
+ #
176
+ # @param (see .dbus_attr_accessor)
177
+ # @return (see .dbus_attr_accessor)
178
+ def self.dbus_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
179
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
180
+
181
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
182
+ property = Property.new(dbus_name, type, :readwrite, ruby_name: ruby_name)
183
+ @@cur_intf.define(property)
184
+
185
+ dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
186
+ end
187
+
188
+ # A read-only property accessing a reader method (which must already exist).
189
+ # (To directly access an instance variable, use {.dbus_attr_reader} instead)
190
+ #
191
+ # At the D-Bus side the property is read only but it makes perfect sense to
192
+ # implement it with a read-write attr_accessor. In that case this method
193
+ # uses {.dbus_watcher} to set up the PropertiesChanged signal.
194
+ #
195
+ # attr_accessor :foo_bar
196
+ # dbus_reader :foo_bar, "s"
197
+ #
198
+ # If the property value should change by other means than its attr_writer,
199
+ # you should emit the `PropertiesChanged` signal by calling
200
+ # {#dbus_properties_changed}.
201
+ #
202
+ # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
203
+ #
204
+ # or, omitting the value in the signal,
205
+ #
206
+ # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
207
+ #
208
+ # @param (see .dbus_attr_accessor)
209
+ # @return (see .dbus_attr_accessor)
210
+ def self.dbus_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
211
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
212
+
213
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
214
+ property = Property.new(dbus_name, type, :read, ruby_name: ruby_name)
215
+ @@cur_intf.define(property)
216
+
217
+ ruby_name_eq = "#{ruby_name}=".to_sym
218
+ return unless method_defined?(ruby_name_eq)
219
+
220
+ dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
221
+ end
222
+
223
+ # A write-only property accessing a writer method (which must already exist).
224
+ # (To directly access an instance variable, use {.dbus_attr_writer} instead)
225
+ #
226
+ # Uses {.dbus_watcher} to set up the PropertiesChanged signal.
227
+ #
228
+ # @param (see .dbus_attr_accessor)
229
+ # @return (see .dbus_attr_accessor)
230
+ def self.dbus_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
231
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
232
+
233
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
234
+ property = Property.new(dbus_name, type, :write, ruby_name: ruby_name)
235
+ @@cur_intf.define(property)
236
+
237
+ dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
238
+ end
239
+
240
+ # Enables automatic sending of the PropertiesChanged signal.
241
+ # For *ruby_name* `foo_bar`, wrap `foo_bar=` so that it sends
242
+ # the signal for FooBar.
243
+ # The original version remains as `_original_foo_bar=`.
244
+ #
245
+ # @param ruby_name [Symbol] :foo_bar and :foo_bar= both mean the same thing
246
+ # @param dbus_name [String] if not given it is made
247
+ # by CamelCasing the ruby_name. foo_bar becomes FooBar
248
+ # to convert the Ruby convention to the DBus convention.
249
+ # @param emits_changed_signal [true,false,:const,:invalidates]
250
+ # see {EmitsChangedSignal}; if unspecified, ask the interface.
251
+ # @return [void]
252
+ def self.dbus_watcher(ruby_name, dbus_name: nil, emits_changed_signal: nil)
253
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
254
+
255
+ interface_name = @@cur_intf.name
256
+
257
+ ruby_name = ruby_name.to_s.sub(/=$/, "").to_sym
258
+ ruby_name_eq = "#{ruby_name}=".to_sym
259
+ original_ruby_name_eq = "_original_#{ruby_name_eq}"
260
+
261
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
262
+
263
+ emits_changed_signal = EmitsChangedSignal.new(emits_changed_signal, interface: @@cur_intf)
264
+
265
+ # the argument order is alias_method(new_name, existing_name)
266
+ alias_method original_ruby_name_eq, ruby_name_eq
267
+ define_method ruby_name_eq do |value|
268
+ result = public_send(original_ruby_name_eq, value)
269
+
270
+ case emits_changed_signal.value
271
+ when true
272
+ # signature: "interface:s, changed_props:a{sv}, invalidated_props:as"
273
+ dbus_properties_changed(interface_name, { dbus_name.to_s => value }, [])
274
+ when :invalidates
275
+ dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
276
+ when :const
277
+ # Oh my, seeing a value change of a supposedly constant property.
278
+ # Maybe should have raised at declaration time, don't make a fuss now.
279
+ when false
280
+ # Do nothing
281
+ end
282
+
283
+ result
92
284
  end
93
285
  end
94
286
 
95
287
  # Defines an exportable method on the object with the given name _sym_,
96
288
  # _prototype_ and the code in a block.
97
- def self.dbus_method(sym, protoype = "", &block)
289
+ # @param prototype [Prototype]
290
+ def self.dbus_method(sym, prototype = "", &block)
98
291
  raise UndefinedInterface, sym if @@cur_intf.nil?
99
- @@cur_intf.define(Method.new(sym.to_s).from_prototype(protoype))
100
- define_method(Object.make_method_name(@@cur_intf.name, sym.to_s), &block)
292
+
293
+ @@cur_intf.define(Method.new(sym.to_s).from_prototype(prototype))
294
+
295
+ ruby_name = Object.make_method_name(@@cur_intf.name, sym.to_s)
296
+ # ::Module#define_method(name) { body }
297
+ define_method(ruby_name, &block)
101
298
  end
102
299
 
103
300
  # Emits a signal from the object with the given _interface_, signal
104
301
  # _sig_ and arguments _args_.
302
+ # @param intf [Interface]
303
+ # @param sig [Signal]
304
+ # @param args arguments for the signal
105
305
  def emit(intf, sig, *args)
106
306
  @service.bus.emit(@service, self, intf, sig, *args)
107
307
  end
108
308
 
109
309
  # Defines a signal for the object with a given name _sym_ and _prototype_.
110
- def self.dbus_signal(sym, protoype = "")
310
+ def self.dbus_signal(sym, prototype = "")
111
311
  raise UndefinedInterface, sym if @@cur_intf.nil?
312
+
112
313
  cur_intf = @@cur_intf
113
- signal = Signal.new(sym.to_s).from_prototype(protoype)
114
- cur_intf.define(Signal.new(sym.to_s).from_prototype(protoype))
314
+ signal = Signal.new(sym.to_s).from_prototype(prototype)
315
+ cur_intf.define(Signal.new(sym.to_s).from_prototype(prototype))
316
+
317
+ # ::Module#define_method(name) { body }
115
318
  define_method(sym.to_s) do |*args|
116
319
  emit(cur_intf, signal, *args)
117
320
  end
118
321
  end
119
322
 
120
- ####################################################################
121
-
122
323
  # Helper method that returns a method name generated from the interface
123
324
  # name _intfname_ and method name _methname_.
124
325
  # @api private
125
326
  def self.make_method_name(intfname, methname)
126
327
  "#{intfname}%%#{methname}"
127
328
  end
329
+
330
+ # TODO: borrow a proven implementation
331
+ # @param str [String]
332
+ # @return [String]
333
+ # @api private
334
+ def self.camelize(str)
335
+ str.split(/_/).map(&:capitalize).join("")
336
+ end
337
+
338
+ # Make a D-Bus conventional name, CamelCased.
339
+ # @param ruby_name [String,Symbol] eg :do_something
340
+ # @param dbus_name [String,Symbol,nil] use this if given
341
+ # @return [Symbol] eg DoSomething
342
+ def self.make_dbus_name(ruby_name, dbus_name: nil)
343
+ dbus_name ||= camelize(ruby_name.to_s)
344
+ dbus_name.to_sym
345
+ end
346
+
347
+ # Use this instead of calling PropertiesChanged directly. This one
348
+ # considers not only the PC signature (which says that all property values
349
+ # are variants) but also the specific property type.
350
+ # @param interface_name [String] interface name like "org.example.ManagerManager"
351
+ # @param changed_props [Hash{String => ::Object}]
352
+ # changed properties (D-Bus names) and their values.
353
+ # @param invalidated_props [Array<String>]
354
+ # names of properties whose changed value is not specified
355
+ def dbus_properties_changed(interface_name, changed_props, invalidated_props)
356
+ typed_changed_props = changed_props.map do |dbus_name, value|
357
+ property = dbus_lookup_property(interface_name, dbus_name)
358
+ type = property.type
359
+ typed_value = Data.make_typed(type, value)
360
+ variant = Data::Variant.new(typed_value, member_type: type)
361
+ [dbus_name, variant]
362
+ end.to_h
363
+ PropertiesChanged(interface_name, typed_changed_props, invalidated_props)
364
+ end
365
+
366
+ # @param interface_name [String]
367
+ # @param property_name [String]
368
+ # @return [Property]
369
+ # @raise [DBus::Error]
370
+ # @api private
371
+ def dbus_lookup_property(interface_name, property_name)
372
+ # what should happen for unknown properties
373
+ # plasma: InvalidArgs (propname), UnknownInterface (interface)
374
+ # systemd: UnknownProperty
375
+ interface = intfs[interface_name]
376
+ if !interface
377
+ raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
378
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found: no such interface"
379
+ end
380
+
381
+ property = interface.properties[property_name.to_sym]
382
+ if !property
383
+ raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
384
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found"
385
+ end
386
+
387
+ property
388
+ end
389
+
390
+ ####################################################################
391
+
392
+ # use the above defined methods to declare the property-handling
393
+ # interfaces and methods
394
+
395
+ dbus_interface PROPERTY_INTERFACE do
396
+ dbus_method :Get, "in interface_name:s, in property_name:s, out value:v" do |interface_name, property_name|
397
+ property = dbus_lookup_property(interface_name, property_name)
398
+
399
+ if property.readable?
400
+ ruby_name = property.ruby_name
401
+ value = public_send(ruby_name)
402
+ # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
403
+ typed_value = Data.make_typed(property.type, value)
404
+ [typed_value]
405
+ else
406
+ raise DBus.error("org.freedesktop.DBus.Error.PropertyWriteOnly"),
407
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not readable"
408
+ end
409
+ end
410
+
411
+ dbus_method :Set, "in interface_name:s, in property_name:s, in val:v" do |interface_name, property_name, value|
412
+ property = dbus_lookup_property(interface_name, property_name)
413
+
414
+ if property.writable?
415
+ ruby_name_eq = "#{property.ruby_name}="
416
+ # TODO: declare dbus_method :Set to take :exact argument
417
+ # and type check it here before passing its :plain value
418
+ # to the implementation
419
+ public_send(ruby_name_eq, value)
420
+ else
421
+ raise DBus.error("org.freedesktop.DBus.Error.PropertyReadOnly"),
422
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not writable"
423
+ end
424
+ end
425
+
426
+ dbus_method :GetAll, "in interface_name:s, out value:a{sv}" do |interface_name|
427
+ interface = intfs[interface_name]
428
+ if !interface
429
+ raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
430
+ "Properties '#{interface_name}.*' (on object '#{@path}') not found: no such interface"
431
+ end
432
+
433
+ p_hash = {}
434
+ interface.properties.each do |p_name, property|
435
+ next unless property.readable?
436
+
437
+ ruby_name = property.ruby_name
438
+ begin
439
+ # D-Bus spec says:
440
+ # > If GetAll is called with a valid interface name for which some
441
+ # > properties are not accessible to the caller (for example, due
442
+ # > to per-property access control implemented in the service),
443
+ # > those properties should be silently omitted from the result
444
+ # > array.
445
+ # so we will silently omit properties that fail to read.
446
+ # Get'ting them individually will send DBus.Error
447
+ value = public_send(ruby_name)
448
+ # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
449
+ typed_value = Data.make_typed(property.type, value)
450
+ p_hash[p_name.to_s] = typed_value
451
+ rescue StandardError
452
+ DBus.logger.debug "Property '#{interface_name}.#{p_name}' (on object '#{@path}')" \
453
+ " has raised during GetAll, omitting it"
454
+ end
455
+ end
456
+
457
+ [p_hash]
458
+ end
459
+
460
+ dbus_signal :PropertiesChanged, "interface:s, changed_properties:a{sv}, invalidated_properties:as"
461
+ end
128
462
  end
129
463
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file is part of the ruby-dbus project
2
4
  # Copyright (C) 2019 Martin Vidner
3
5
  #
@@ -7,18 +9,21 @@
7
9
  # See the file "COPYING" for the exact licensing terms.
8
10
 
9
11
  module DBus
10
- # A {::String} that validates at initialization time
12
+ # A {::String} that validates at initialization time.
13
+ # See also {DBus::Data::ObjectPath}
14
+ # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
11
15
  class ObjectPath < String
12
16
  # @raise Error if not a valid object path
13
- def initialize(s)
14
- unless self.class.valid?(s)
15
- raise DBus::Error, "Invalid object path #{s.inspect}"
17
+ def initialize(str)
18
+ unless self.class.valid?(str)
19
+ raise DBus::Error, "Invalid object path #{str.inspect}"
16
20
  end
21
+
17
22
  super
18
23
  end
19
24
 
20
- def self.valid?(s)
21
- s == "/" || s =~ %r{\A(/[A-Za-z0-9_]+)+\z}
25
+ def self.valid?(str)
26
+ str == "/" || str =~ %r{\A(/[A-Za-z0-9_]+)+\z}
22
27
  end
23
28
  end
24
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file is part of the ruby-dbus project
2
4
  # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
3
5
  # Copyright (C) 2009-2014 Martin Vidner
@@ -44,6 +46,7 @@ module DBus
44
46
  end
45
47
 
46
48
  # Returns the interfaces of the object.
49
+ # @return [Array<String>] names of the interfaces
47
50
  def interfaces
48
51
  introspect unless introspected
49
52
  @interfaces.keys
@@ -56,6 +59,7 @@ module DBus
56
59
  introspect unless introspected
57
60
  ifc = @interfaces[intfname]
58
61
  raise DBus::Error, "no such interface `#{intfname}' on object `#{@path}'" unless ifc
62
+
59
63
  ifc
60
64
  end
61
65
 
@@ -92,6 +96,7 @@ module DBus
92
96
  # don't overwrite instance methods!
93
97
  next if dup_meths.include?(name)
94
98
  next if self.class.instance_methods.include?(name)
99
+
95
100
  if univocal_meths.include? name
96
101
  univocal_meths.delete name
97
102
  dup_meths << name
@@ -123,12 +128,20 @@ module DBus
123
128
  def on_signal(name, &block)
124
129
  # TODO: improve
125
130
  raise NoMethodError unless @default_iface && has_iface?(@default_iface)
131
+
126
132
  @interfaces[@default_iface].on_signal(name, &block)
127
133
  end
128
134
 
129
135
  ####################################################
130
136
  private
131
137
 
138
+ # rubocop:disable Lint/MissingSuper
139
+ # as this should forward everything
140
+ #
141
+ # https://github.com/rubocop-hq/ruby-style-guide#no-method-missing
142
+ # and http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/
143
+ # have a point to be investigated
144
+
132
145
  # Handles all unkown methods, mostly to route method calls to the
133
146
  # default interface.
134
147
  def method_missing(name, *args, &reply_handler)
@@ -146,9 +159,17 @@ module DBus
146
159
  # interesting, foo.method("unknown")
147
160
  # raises NameError, not NoMethodError
148
161
  raise unless e.to_s =~ /undefined method `#{name}'/
162
+
149
163
  # BTW e.exception("...") would preserve the class.
150
164
  raise NoMethodError, "undefined method `#{name}' for DBus interface `#{@default_iface}' on object `#{@path}'"
151
165
  end
152
166
  end
153
- end # class ProxyObject
167
+ # rubocop:enable Lint/MissingSuper
168
+
169
+ def respond_to_missing?(name, _include_private = false)
170
+ @default_iface &&
171
+ has_iface?(@default_iface) &&
172
+ @interfaces[@default_iface].methods.key?(name) or super
173
+ end
174
+ end
154
175
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file is part of the ruby-dbus project
2
4
  # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
3
5
  # Copyright (C) 2009-2014 Martin Vidner
@@ -22,17 +24,21 @@ module DBus
22
24
  @api = api
23
25
  end
24
26
 
25
- # Investigates the sub-nodes of the proxy object _po_ based on the
27
+ # Investigates the sub-nodes of the proxy object _pobj_ based on the
26
28
  # introspection XML data _xml_ and sets them up recursively.
27
- def self.introspect_into(po, xml)
28
- intfs, po.subnodes = IntrospectXMLParser.new(xml).parse
29
+ # @param pobj [ProxyObject]
30
+ # @param xml [String]
31
+ def self.introspect_into(pobj, xml)
32
+ # intfs [Array<Interface>], subnodes [Array<String>]
33
+ intfs, pobj.subnodes = IntrospectXMLParser.new(xml).parse
29
34
  intfs.each do |i|
30
- poi = ProxyObjectInterface.new(po, i.name)
35
+ poi = ProxyObjectInterface.new(pobj, i.name)
31
36
  i.methods.each_value { |m| poi.define(m) }
32
37
  i.signals.each_value { |s| poi.define(s) }
33
- po[i.name] = poi
38
+ i.properties.each_value { |p| poi.define(p) }
39
+ pobj[i.name] = poi
34
40
  end
35
- po.introspected = true
41
+ pobj.introspected = true
36
42
  end
37
43
 
38
44
  # Generates, sets up and returns the proxy object.
@@ -41,5 +47,5 @@ module DBus
41
47
  ProxyObjectFactory.introspect_into(po, @xml)
42
48
  po
43
49
  end
44
- end # class ProxyObjectFactory
50
+ end
45
51
  end