ruby-dbus 0.16.0 → 0.25.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS.md +288 -3
  3. data/README.md +3 -5
  4. data/Rakefile +27 -12
  5. data/VERSION +1 -1
  6. data/doc/Reference.md +109 -10
  7. data/examples/doc/_extract_examples +7 -0
  8. data/examples/no-bus/pulseaudio.rb +50 -0
  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 +3 -3
  15. data/examples/simple/call_introspect.rb +1 -0
  16. data/examples/simple/get_id.rb +6 -3
  17. data/examples/simple/properties.rb +2 -0
  18. data/examples/utils/listnames.rb +10 -1
  19. data/examples/utils/notify.rb +1 -0
  20. data/lib/dbus/api_options.rb +9 -0
  21. data/lib/dbus/auth.rb +307 -217
  22. data/lib/dbus/bus.rb +155 -588
  23. data/lib/dbus/bus_name.rb +12 -8
  24. data/lib/dbus/connection.rb +363 -0
  25. data/lib/dbus/core_ext/class/attribute.rb +1 -1
  26. data/lib/dbus/data.rb +821 -0
  27. data/lib/dbus/emits_changed_signal.rb +83 -0
  28. data/lib/dbus/error.rb +4 -2
  29. data/lib/dbus/introspect.rb +133 -31
  30. data/lib/dbus/logger.rb +6 -3
  31. data/lib/dbus/main.rb +66 -0
  32. data/lib/dbus/marshall.rb +247 -296
  33. data/lib/dbus/matchrule.rb +16 -16
  34. data/lib/dbus/message.rb +66 -57
  35. data/lib/dbus/message_queue.rb +40 -25
  36. data/lib/dbus/node_tree.rb +105 -0
  37. data/lib/dbus/object.rb +442 -33
  38. data/lib/dbus/object_manager.rb +61 -0
  39. data/lib/dbus/object_path.rb +11 -6
  40. data/lib/dbus/object_server.rb +149 -0
  41. data/lib/dbus/org.freedesktop.DBus.xml +97 -0
  42. data/lib/dbus/platform.rb +26 -0
  43. data/lib/dbus/proxy_object.rb +30 -11
  44. data/lib/dbus/proxy_object_factory.rb +13 -7
  45. data/lib/dbus/proxy_object_interface.rb +63 -30
  46. data/lib/dbus/proxy_service.rb +107 -0
  47. data/lib/dbus/raw_message.rb +91 -0
  48. data/lib/dbus/type.rb +319 -86
  49. data/lib/dbus/xml.rb +40 -19
  50. data/lib/dbus.rb +28 -17
  51. data/ruby-dbus.gemspec +13 -6
  52. data/spec/async_spec.rb +2 -0
  53. data/spec/auth_spec.rb +225 -0
  54. data/spec/binding_spec.rb +2 -0
  55. data/spec/bus_and_xml_backend_spec.rb +5 -1
  56. data/spec/bus_connection_spec.rb +93 -0
  57. data/spec/bus_name_spec.rb +3 -1
  58. data/spec/bus_spec.rb +2 -0
  59. data/spec/byte_array_spec.rb +2 -0
  60. data/spec/client_robustness_spec.rb +4 -2
  61. data/spec/connection_spec.rb +37 -0
  62. data/spec/coverage_helper.rb +39 -0
  63. data/spec/data/marshall.yaml +1667 -0
  64. data/spec/data_spec.rb +673 -0
  65. data/spec/dbus_spec.rb +22 -0
  66. data/spec/emits_changed_signal_spec.rb +58 -0
  67. data/spec/err_msg_spec.rb +2 -0
  68. data/spec/introspect_xml_parser_spec.rb +2 -0
  69. data/spec/introspection_spec.rb +2 -0
  70. data/spec/main_loop_spec.rb +17 -1
  71. data/spec/message_spec.rb +21 -0
  72. data/spec/mock-service/cockpit-dbustests.rb +29 -0
  73. data/spec/mock-service/com.redhat.Cockpit.DBusTests.xml +180 -0
  74. data/spec/mock-service/org.ruby.service.service +4 -0
  75. data/spec/mock-service/org.rubygems.ruby_dbus.DBusTests.service +4 -0
  76. data/spec/{service_newapi.rb → mock-service/spaghetti-monster.rb} +122 -75
  77. data/spec/node_spec.rb +65 -0
  78. data/spec/object_manager_spec.rb +33 -0
  79. data/spec/object_path_spec.rb +3 -0
  80. data/spec/object_server_spec.rb +138 -0
  81. data/spec/object_spec.rb +194 -0
  82. data/spec/packet_marshaller_spec.rb +41 -0
  83. data/spec/packet_unmarshaller_spec.rb +248 -0
  84. data/spec/platform_spec.rb +14 -0
  85. data/spec/property_spec.rb +214 -12
  86. data/spec/proxy_object_interface_spec.rb +35 -0
  87. data/spec/proxy_object_spec.rb +2 -0
  88. data/spec/{bus_driver_spec.rb → proxy_service_spec.rb} +15 -8
  89. data/spec/raw_message_spec.rb +32 -0
  90. data/spec/server_robustness_spec.rb +20 -6
  91. data/spec/session_bus_spec.rb +36 -1
  92. data/spec/session_bus_spec_manual.rb +2 -0
  93. data/spec/signal_spec.rb +16 -3
  94. data/spec/spec_helper.rb +25 -33
  95. data/spec/thread_safety_spec.rb +55 -12
  96. data/spec/tools/dbus-launch-simple +9 -6
  97. data/spec/tools/dbus-limited-session.conf +32 -0
  98. data/spec/tools/test_env +26 -6
  99. data/spec/type_spec.rb +214 -6
  100. data/spec/value_spec.rb +16 -1
  101. data/spec/variant_spec.rb +4 -2
  102. data/spec/zzz_quit_spec.rb +16 -0
  103. metadata +92 -20
  104. data/examples/gdbus/gdbus +0 -257
  105. data/examples/gdbus/gdbus.glade +0 -98
  106. data/examples/gdbus/launch.sh +0 -4
  107. data/spec/server_spec.rb +0 -53
  108. data/spec/tools/test_server +0 -39
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,46 +8,65 @@
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
  #
16
19
  # Objects that are going to be exported by a D-Bus service
17
20
  # should inherit from this class. At the client side, use {ProxyObject}.
18
21
  class Object
19
- # The path of the object.
22
+ # @return [ObjectPath] 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 = {}
24
28
 
25
- # The service that the object is exported by.
26
- attr_writer :service
27
-
28
29
  @@cur_intf = nil # Interface
29
30
  @@intfs_mutex = Mutex.new
30
31
 
31
32
  # Create a new object with a given _path_.
32
- # Use Service#export to export it.
33
+ # Use ObjectServer#export to export it.
34
+ # @param path [ObjectPath] The path of the object.
33
35
  def initialize(path)
34
36
  @path = path
35
- @service = nil
37
+ # TODO: what parts of our API are supposed to work before we're exported?
38
+ self.object_server = nil
39
+ end
40
+
41
+ # @return [ObjectServer] the server the object is exported by
42
+ def object_server
43
+ # tests may mock the old ivar
44
+ @object_server || @service
45
+ end
46
+
47
+ # @param server [ObjectServer] the server the object is exported by
48
+ # @note only the server itself should call this in its #export/#unexport
49
+ def object_server=(server)
50
+ # until v0.22.1 there was attr_writer :service
51
+ # so subclasses only could use @service
52
+ @object_server = @service = server
36
53
  end
37
54
 
38
55
  # Dispatch a message _msg_ to call exported methods
56
+ # @param msg [Message] only METHOD_CALLS do something
57
+ # @api private
39
58
  def dispatch(msg)
40
59
  case msg.message_type
41
60
  when Message::METHOD_CALL
42
61
  reply = nil
43
62
  begin
44
- if !intfs[msg.interface]
63
+ iface = intfs[msg.interface]
64
+ if !iface
45
65
  raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
46
66
  "Interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
47
67
  end
48
- meth = intfs[msg.interface].methods[msg.member.to_sym]
68
+ member_sym = msg.member.to_sym
69
+ meth = iface.methods[member_sym]
49
70
  if !meth
50
71
  raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
51
72
  "Method \"#{msg.member}\" on interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
@@ -55,75 +76,463 @@ module DBus
55
76
  retdata = [*retdata]
56
77
 
57
78
  reply = Message.method_return(msg)
58
- meth.rets.zip(retdata).each do |rsig, rdata|
59
- reply.add_param(rsig.type, rdata)
79
+ rsigs = meth.rets.map(&:type)
80
+ rsigs.zip(retdata).each do |rsig, rdata|
81
+ reply.add_param(rsig, rdata)
60
82
  end
61
- rescue => ex
62
- dbus_msg_exc = msg.annotate_exception(ex)
83
+ rescue StandardError => e
84
+ dbus_msg_exc = msg.annotate_exception(e)
63
85
  reply = ErrorMessage.from_exception(dbus_msg_exc).reply_to(msg)
64
86
  end
65
- @service.bus.message_queue.push(reply)
87
+ # TODO: this method chain is too long,
88
+ # we should probably just return reply [Message] like we get a [Message]
89
+ object_server.connection.message_queue.push(reply)
66
90
  end
67
91
  end
68
92
 
69
93
  # Select (and create) the interface that the following defined methods
70
94
  # belong to.
71
- def self.dbus_interface(s)
95
+ # @param name [String] interface name like "org.example.ManagerManager"
96
+ # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface
97
+ def self.dbus_interface(name)
72
98
  @@intfs_mutex.synchronize do
73
- @@cur_intf = intfs[s]
99
+ @@cur_intf = intfs[name]
74
100
  if !@@cur_intf
75
- @@cur_intf = Interface.new(s)
101
+ @@cur_intf = Interface.new(name) # validates the name
76
102
  # As this is a mutable class_attr, we cannot use
77
- # self.intfs[s] = @@cur_intf # Hash#[]=
103
+ # self.intfs[name] = @@cur_intf # Hash#[]=
78
104
  # as that would modify parent class attr in place.
79
105
  # Using the setter lets a subclass have the new value
80
106
  # while the superclass keeps the old one.
81
- self.intfs = intfs.merge(s => @@cur_intf)
107
+ self.intfs = intfs.merge(name => @@cur_intf)
108
+ end
109
+ begin
110
+ yield
111
+ ensure
112
+ @@cur_intf = nil
82
113
  end
83
- yield
84
- @@cur_intf = nil
85
114
  end
86
115
  end
87
116
 
88
- # Dummy undefined interface class.
117
+ # Forgetting to declare the interface for a method/signal/property
118
+ # is a ScriptError.
89
119
  class UndefinedInterface < ScriptError
90
120
  def initialize(sym)
91
- super "No interface specified for #{sym}"
121
+ super "No interface specified for #{sym}. Enclose it in dbus_interface."
122
+ end
123
+ end
124
+
125
+ # Declare the behavior of PropertiesChanged signal,
126
+ # common for all properties in this interface
127
+ # (individual properties may override it)
128
+ # @example
129
+ # self.emits_changed_signal = :invalidates
130
+ # @param [true,false,:const,:invalidates] value
131
+ def self.emits_changed_signal=(value)
132
+ raise UndefinedInterface, :emits_changed_signal if @@cur_intf.nil?
133
+
134
+ @@cur_intf.emits_changed_signal = EmitsChangedSignal.new(value)
135
+ end
136
+
137
+ # A read-write property accessing an instance variable.
138
+ # A combination of `attr_accessor` and {.dbus_accessor}.
139
+ #
140
+ # PropertiesChanged signal will be emitted whenever `foo_bar=` is used
141
+ # but not when @foo_bar is written directly.
142
+ #
143
+ # @param ruby_name [Symbol] :foo_bar is exposed as FooBar;
144
+ # use dbus_name to override
145
+ # @param type [Type,SingleCompleteType]
146
+ # a signature like "s" or "a(uus)" or Type::STRING
147
+ # @param dbus_name [String] if not given it is made
148
+ # by CamelCasing the ruby_name. foo_bar becomes FooBar
149
+ # to convert the Ruby convention to the DBus convention.
150
+ # @param emits_changed_signal [true,false,:const,:invalidates]
151
+ # see {EmitsChangedSignal}; if unspecified, ask the interface.
152
+ # @return [void]
153
+ def self.dbus_attr_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
154
+ attr_accessor(ruby_name)
155
+
156
+ dbus_accessor(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
157
+ end
158
+
159
+ # A read-only property accessing a read-write instance variable.
160
+ # A combination of `attr_accessor` and {.dbus_reader}.
161
+ #
162
+ # @param (see .dbus_attr_accessor)
163
+ # @return (see .dbus_attr_accessor)
164
+ def self.dbus_reader_attr_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
165
+ attr_accessor(ruby_name)
166
+
167
+ dbus_reader(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
168
+ end
169
+
170
+ # A read-only property accessing an instance variable.
171
+ # A combination of `attr_reader` and {.dbus_reader}.
172
+ #
173
+ # You may be instead looking for a variant which is read-write from the Ruby side:
174
+ # {.dbus_reader_attr_accessor}.
175
+ #
176
+ # Whenever the property value gets changed from "inside" the object,
177
+ # you should emit the `PropertiesChanged` signal by calling
178
+ # {#dbus_properties_changed}.
179
+ #
180
+ # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
181
+ #
182
+ # or, omitting the value in the signal,
183
+ #
184
+ # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
185
+ #
186
+ # @param (see .dbus_attr_accessor)
187
+ # @return (see .dbus_attr_accessor)
188
+ def self.dbus_attr_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
189
+ attr_reader(ruby_name)
190
+
191
+ dbus_reader(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
192
+ end
193
+
194
+ # A write-only property accessing an instance variable.
195
+ # A combination of `attr_writer` and {.dbus_writer}.
196
+ #
197
+ # @param (see .dbus_attr_accessor)
198
+ # @return (see .dbus_attr_accessor)
199
+ def self.dbus_attr_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
200
+ attr_writer(ruby_name)
201
+
202
+ dbus_writer(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
203
+ end
204
+
205
+ # A read-write property using a pair of reader/writer methods
206
+ # (which must already exist).
207
+ # (To directly access an instance variable, use {.dbus_attr_accessor} instead)
208
+ #
209
+ # Uses {.dbus_watcher} to set up the PropertiesChanged signal.
210
+ #
211
+ # @param (see .dbus_attr_accessor)
212
+ # @return (see .dbus_attr_accessor)
213
+ def self.dbus_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
214
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
215
+
216
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
217
+ property = Property.new(dbus_name, type, :readwrite, ruby_name: ruby_name)
218
+ @@cur_intf.define(property)
219
+
220
+ dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
221
+ end
222
+
223
+ # A read-only property accessing a reader method (which must already exist).
224
+ # (To directly access an instance variable, use {.dbus_attr_reader} instead)
225
+ #
226
+ # At the D-Bus side the property is read only but it makes perfect sense to
227
+ # implement it with a read-write attr_accessor. In that case this method
228
+ # uses {.dbus_watcher} to set up the PropertiesChanged signal.
229
+ #
230
+ # attr_accessor :foo_bar
231
+ # dbus_reader :foo_bar, "s"
232
+ #
233
+ # The above two declarations have a shorthand:
234
+ #
235
+ # dbus_reader_attr_accessor :foo_bar, "s"
236
+ #
237
+ # If the property value should change by other means than its attr_writer,
238
+ # you should emit the `PropertiesChanged` signal by calling
239
+ # {#dbus_properties_changed}.
240
+ #
241
+ # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
242
+ #
243
+ # or, omitting the value in the signal,
244
+ #
245
+ # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
246
+ #
247
+ # @param (see .dbus_attr_accessor)
248
+ # @return (see .dbus_attr_accessor)
249
+ def self.dbus_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
250
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
251
+
252
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
253
+ property = Property.new(dbus_name, type, :read, ruby_name: ruby_name)
254
+ @@cur_intf.define(property)
255
+
256
+ ruby_name_eq = :"#{ruby_name}="
257
+ return unless method_defined?(ruby_name_eq)
258
+
259
+ dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
260
+ end
261
+
262
+ # A write-only property accessing a writer method (which must already exist).
263
+ # (To directly access an instance variable, use {.dbus_attr_writer} instead)
264
+ #
265
+ # Uses {.dbus_watcher} to set up the PropertiesChanged signal.
266
+ #
267
+ # @param (see .dbus_attr_accessor)
268
+ # @return (see .dbus_attr_accessor)
269
+ def self.dbus_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
270
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
271
+
272
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
273
+ property = Property.new(dbus_name, type, :write, ruby_name: ruby_name)
274
+ @@cur_intf.define(property)
275
+
276
+ dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
277
+ end
278
+
279
+ # Enables automatic sending of the PropertiesChanged signal.
280
+ # For *ruby_name* `foo_bar`, wrap `foo_bar=` so that it sends
281
+ # the signal for FooBar.
282
+ # The original version remains as `_original_foo_bar=`.
283
+ #
284
+ # @param ruby_name [Symbol] :foo_bar and :foo_bar= both mean the same thing
285
+ # @param dbus_name [String] if not given it is made
286
+ # by CamelCasing the ruby_name. foo_bar becomes FooBar
287
+ # to convert the Ruby convention to the DBus convention.
288
+ # @param emits_changed_signal [true,false,:const,:invalidates]
289
+ # see {EmitsChangedSignal}; if unspecified, ask the interface.
290
+ # @return [void]
291
+ def self.dbus_watcher(ruby_name, dbus_name: nil, emits_changed_signal: nil)
292
+ raise UndefinedInterface, ruby_name if @@cur_intf.nil?
293
+
294
+ interface_name = @@cur_intf.name
295
+
296
+ ruby_name = ruby_name.to_s.sub(/=$/, "").to_sym
297
+ ruby_name_eq = :"#{ruby_name}="
298
+ original_ruby_name_eq = "_original_#{ruby_name_eq}"
299
+
300
+ dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
301
+
302
+ emits_changed_signal = EmitsChangedSignal.new(emits_changed_signal, interface: @@cur_intf)
303
+
304
+ # the argument order is alias_method(new_name, existing_name)
305
+ alias_method original_ruby_name_eq, ruby_name_eq
306
+ define_method ruby_name_eq do |value|
307
+ result = public_send(original_ruby_name_eq, value)
308
+
309
+ case emits_changed_signal.value
310
+ when true
311
+ # signature: "interface:s, changed_props:a{sv}, invalidated_props:as"
312
+ dbus_properties_changed(interface_name, { dbus_name.to_s => value }, [])
313
+ when :invalidates
314
+ dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
315
+ when :const
316
+ # Oh my, seeing a value change of a supposedly constant property.
317
+ # Maybe should have raised at declaration time, don't make a fuss now.
318
+ when false
319
+ # Do nothing
320
+ end
321
+
322
+ result
92
323
  end
93
324
  end
94
325
 
95
326
  # Defines an exportable method on the object with the given name _sym_,
96
327
  # _prototype_ and the code in a block.
97
- def self.dbus_method(sym, protoype = "", &block)
328
+ # @param prototype [Prototype]
329
+ def self.dbus_method(sym, prototype = "", &block)
98
330
  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)
331
+
332
+ @@cur_intf.define(Method.new(sym.to_s).from_prototype(prototype))
333
+
334
+ ruby_name = Object.make_method_name(@@cur_intf.name, sym.to_s)
335
+ # ::Module#define_method(name) { body }
336
+ define_method(ruby_name, &block)
101
337
  end
102
338
 
103
339
  # Emits a signal from the object with the given _interface_, signal
104
340
  # _sig_ and arguments _args_.
341
+ # @param intf [Interface]
342
+ # @param sig [Signal]
343
+ # @param args arguments for the signal
105
344
  def emit(intf, sig, *args)
106
- @service.bus.emit(@service, self, intf, sig, *args)
345
+ raise "Cannot emit signal #{intf.name}.#{sig.name} before #{path} is exported" if object_server.nil?
346
+
347
+ object_server.connection.emit(nil, self, intf, sig, *args)
107
348
  end
108
349
 
109
350
  # Defines a signal for the object with a given name _sym_ and _prototype_.
110
- def self.dbus_signal(sym, protoype = "")
351
+ def self.dbus_signal(sym, prototype = "")
111
352
  raise UndefinedInterface, sym if @@cur_intf.nil?
353
+
112
354
  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))
355
+ signal = Signal.new(sym.to_s).from_prototype(prototype)
356
+ cur_intf.define(signal)
357
+
358
+ # ::Module#define_method(name) { body }
115
359
  define_method(sym.to_s) do |*args|
116
360
  emit(cur_intf, signal, *args)
117
361
  end
118
362
  end
119
363
 
120
- ####################################################################
121
-
122
364
  # Helper method that returns a method name generated from the interface
123
365
  # name _intfname_ and method name _methname_.
124
366
  # @api private
125
367
  def self.make_method_name(intfname, methname)
126
368
  "#{intfname}%%#{methname}"
127
369
  end
370
+
371
+ # TODO: borrow a proven implementation
372
+ # @param str [String]
373
+ # @return [String]
374
+ # @api private
375
+ def self.camelize(str)
376
+ str.split(/_/).map(&:capitalize).join("")
377
+ end
378
+
379
+ # Make a D-Bus conventional name, CamelCased.
380
+ # @param ruby_name [String,Symbol] eg :do_something
381
+ # @param dbus_name [String,Symbol,nil] use this if given
382
+ # @return [Symbol] eg DoSomething
383
+ def self.make_dbus_name(ruby_name, dbus_name: nil)
384
+ dbus_name ||= camelize(ruby_name.to_s)
385
+ dbus_name.to_sym
386
+ end
387
+
388
+ # Use this instead of calling PropertiesChanged directly. This one
389
+ # considers not only the PC signature (which says that all property values
390
+ # are variants) but also the specific property type.
391
+ # @param interface_name [String] interface name like "org.example.ManagerManager"
392
+ # @param changed_props [Hash{String => ::Object}]
393
+ # changed properties (D-Bus names) and their values.
394
+ # @param invalidated_props [Array<String>]
395
+ # names of properties whose changed value is not specified
396
+ def dbus_properties_changed(interface_name, changed_props, invalidated_props)
397
+ typed_changed_props = changed_props.map do |dbus_name, value|
398
+ property = dbus_lookup_property(interface_name, dbus_name)
399
+ type = property.type
400
+ typed_value = Data.make_typed(type, value)
401
+ variant = Data::Variant.new(typed_value, member_type: type)
402
+ [dbus_name, variant]
403
+ end.to_h
404
+ PropertiesChanged(interface_name, typed_changed_props, invalidated_props)
405
+ end
406
+
407
+ # @param interface_name [String]
408
+ # @param property_name [String]
409
+ # @return [Property]
410
+ # @raise [DBus::Error]
411
+ # @api private
412
+ def dbus_lookup_property(interface_name, property_name)
413
+ # what should happen for unknown properties
414
+ # plasma: InvalidArgs (propname), UnknownInterface (interface)
415
+ # systemd: UnknownProperty
416
+ interface = intfs[interface_name]
417
+ if !interface
418
+ raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
419
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found: no such interface"
420
+ end
421
+
422
+ property = interface.properties[property_name.to_sym]
423
+ if !property
424
+ raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
425
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found"
426
+ end
427
+
428
+ property
429
+ end
430
+
431
+ # Generates information about interfaces and properties of the object
432
+ #
433
+ # Returns a hash containing interfaces names as keys. Each value is the
434
+ # same hash that would be returned by the
435
+ # org.freedesktop.DBus.Properties.GetAll() method for that combination of
436
+ # object path and interface. If an interface has no properties, the empty
437
+ # hash is returned.
438
+ #
439
+ # @return [Hash{String => Hash{String => Data::Base}}] interface -> property -> value
440
+ def interfaces_and_properties
441
+ get_all_method = self.class.make_method_name("org.freedesktop.DBus.Properties", :GetAll)
442
+
443
+ intfs.keys.each_with_object({}) do |interface, hash|
444
+ hash[interface] = public_send(get_all_method, interface).first
445
+ end
446
+ end
447
+
448
+ ####################################################################
449
+
450
+ # use the above defined methods to declare the property-handling
451
+ # interfaces and methods
452
+
453
+ dbus_interface PROPERTY_INTERFACE do
454
+ dbus_method :Get, "in interface_name:s, in property_name:s, out value:v" do |interface_name, property_name|
455
+ property = dbus_lookup_property(interface_name, property_name)
456
+
457
+ if property.readable?
458
+ begin
459
+ ruby_name = property.ruby_name
460
+ value = public_send(ruby_name)
461
+ # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
462
+ typed_value = Data.make_typed(property.type, value)
463
+ [typed_value]
464
+ rescue StandardError => e
465
+ msg = "When getting '#{interface_name}.#{property_name}': " + e.message
466
+ raise e.exception(msg)
467
+ end
468
+ else
469
+ raise DBus.error("org.freedesktop.DBus.Error.PropertyWriteOnly"),
470
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not readable"
471
+ end
472
+ end
473
+
474
+ dbus_method :Set, "in interface_name:s, in property_name:s, in val:v" do |interface_name, property_name, value|
475
+ property = dbus_lookup_property(interface_name, property_name)
476
+
477
+ if property.writable?
478
+ begin
479
+ ruby_name_eq = "#{property.ruby_name}="
480
+ # TODO: declare dbus_method :Set to take :exact argument
481
+ # and type check it here before passing its :plain value
482
+ # to the implementation
483
+ public_send(ruby_name_eq, value)
484
+ rescue StandardError => e
485
+ msg = "When setting '#{interface_name}.#{property_name}': " + e.message
486
+ raise e.exception(msg)
487
+ end
488
+ else
489
+ raise DBus.error("org.freedesktop.DBus.Error.PropertyReadOnly"),
490
+ "Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not writable"
491
+ end
492
+ end
493
+
494
+ dbus_method :GetAll, "in interface_name:s, out value:a{sv}" do |interface_name|
495
+ interface = intfs[interface_name]
496
+ if !interface
497
+ raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
498
+ "Properties '#{interface_name}.*' (on object '#{@path}') not found: no such interface"
499
+ end
500
+
501
+ p_hash = {}
502
+ interface.properties.each do |p_name, property|
503
+ next unless property.readable?
504
+
505
+ ruby_name = property.ruby_name
506
+ begin
507
+ # D-Bus spec says:
508
+ # > If GetAll is called with a valid interface name for which some
509
+ # > properties are not accessible to the caller (for example, due
510
+ # > to per-property access control implemented in the service),
511
+ # > those properties should be silently omitted from the result
512
+ # > array.
513
+ # so we will silently omit properties that fail to read.
514
+ # Get'ting them individually will send DBus.Error
515
+ value = public_send(ruby_name)
516
+ # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
517
+ typed_value = Data.make_typed(property.type, value)
518
+ p_hash[p_name.to_s] = typed_value
519
+ rescue StandardError
520
+ DBus.logger.debug "Property '#{interface_name}.#{p_name}' (on object '#{@path}') " \
521
+ "has raised during GetAll, omitting it"
522
+ end
523
+ end
524
+
525
+ [p_hash]
526
+ end
527
+
528
+ dbus_signal :PropertiesChanged, "interface:s, changed_properties:a{sv}, invalidated_properties:as"
529
+ end
530
+
531
+ dbus_interface "org.freedesktop.DBus.Introspectable" do
532
+ dbus_method :Introspect, "out xml_data:s" do
533
+ # The body is not used, Connection#process handles it instead
534
+ # which is more efficient and handles paths without objects.
535
+ end
536
+ end
128
537
  end
129
538
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of the ruby-dbus project
4
+ # Copyright (C) 2022 José Iván López González
5
+ # Copyright (C) 2022 Martin Vidner
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License, version 2.1 as published by the Free Software Foundation.
10
+ # See the file "COPYING" for the exact licensing terms.
11
+
12
+ module DBus
13
+ # A mixin for {DBus::Object} implementing
14
+ # {https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
15
+ # org.freedesktop.DBus.ObjectManager}.
16
+ #
17
+ # {ObjectServer#export} and {ObjectServer#unexport} will look for an ObjectManager
18
+ # parent in the path hierarchy. If found, it will emit InterfacesAdded
19
+ # or InterfacesRemoved, as appropriate.
20
+ module ObjectManager
21
+ OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"
22
+
23
+ # Implements `the GetManagedObjects` method.
24
+ # @return [Hash{ObjectPath => Hash{String => Hash{String => Data::Base}}}]
25
+ # object -> interface -> property -> value
26
+ def managed_objects
27
+ descendant_objects = object_server.descendants_for(path)
28
+ descendant_objects.each_with_object({}) do |obj, hash|
29
+ hash[obj.path] = obj.interfaces_and_properties
30
+ end
31
+ end
32
+
33
+ # {ObjectServer#export} will call this for you to emit the `InterfacesAdded` signal.
34
+ # @param object [DBus::Object]
35
+ # @return [void]
36
+ def object_added(object)
37
+ InterfacesAdded(object.path, object.interfaces_and_properties)
38
+ end
39
+
40
+ # {ObjectServer#unexport} will call this for you to emit the `InterfacesRemoved` signal.
41
+ # @param object [DBus::Object]
42
+ # @return [void]
43
+ def object_removed(object)
44
+ InterfacesRemoved(object.path, object.intfs.keys)
45
+ end
46
+
47
+ # Module#included, a hook for `include ObjectManager`, declares its dbus_interface.
48
+ def self.included(base)
49
+ base.class_eval do
50
+ dbus_interface OBJECT_MANAGER_INTERFACE do
51
+ dbus_method :GetManagedObjects, "out res:a{oa{sa{sv}}}" do
52
+ [managed_objects]
53
+ end
54
+
55
+ dbus_signal :InterfacesAdded, "object:o, interfaces_and_properties:a{sa{sv}}"
56
+ dbus_signal :InterfacesRemoved, "object:o, interfaces:as"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ 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