ruby-dbus 0.16.0 → 0.18.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.
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