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/bus_name.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) 2019 Martin Vidner
3
5
  #
@@ -7,21 +9,23 @@
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
+ # D-Bus: a name for a connection, like ":1.3" or "org.example.ManagerManager".
13
+ # Implemented as a {::String} that validates at initialization time.
11
14
  # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus
12
15
  class BusName < String
13
16
  # @raise Error if not a valid bus name
14
- def initialize(s)
15
- unless self.class.valid?(s)
16
- raise DBus::Error, "Invalid bus name #{s.inspect}"
17
+ def initialize(name)
18
+ unless self.class.valid?(name)
19
+ raise DBus::Error, "Invalid bus name #{name.inspect}"
17
20
  end
21
+
18
22
  super
19
23
  end
20
24
 
21
- def self.valid?(s)
22
- s.size <= 255 &&
23
- (s =~ /\A:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+\z/ ||
24
- s =~ /\A[A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+\z/)
25
+ def self.valid?(name)
26
+ name.size <= 255 &&
27
+ (name =~ /\A:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+\z/ ||
28
+ name =~ /\A[A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+\z/)
25
29
  end
26
30
  end
27
31
  end
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of the ruby-dbus project
4
+ # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
5
+ # Copyright (C) 2023 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
+ # D-Bus main connection class
14
+ #
15
+ # Main class that maintains a connection to a bus and can handle incoming
16
+ # and outgoing messages.
17
+ class Connection
18
+ # pop and push messages here
19
+ # @return [MessageQueue]
20
+ attr_reader :message_queue
21
+
22
+ # Create a new connection to the bus for a given connect _path_. _path_
23
+ # format is described in the D-Bus specification:
24
+ # http://dbus.freedesktop.org/doc/dbus-specification.html#addresses
25
+ # and is something like:
26
+ # "transport1:key1=value1,key2=value2;transport2:key1=value1,key2=value2"
27
+ # e.g. "unix:path=/tmp/dbus-test" or "tcp:host=localhost,port=2687"
28
+ def initialize(path)
29
+ @message_queue = MessageQueue.new(path)
30
+
31
+ # @return [Hash{Integer => Proc}]
32
+ # key: message serial
33
+ # value: block to be run when the reply to that message is received
34
+ @method_call_replies = {}
35
+
36
+ # @return [Hash{Integer => Message}]
37
+ # for debugging only: messages for which a reply was not received yet;
38
+ # key == value.serial
39
+ @method_call_msgs = {}
40
+ @signal_matchrules = {}
41
+ end
42
+
43
+ def object_server
44
+ @object_server ||= ObjectServer.new(self)
45
+ end
46
+
47
+ # Dispatch all messages that are available in the queue,
48
+ # but do not block on the queue.
49
+ # Called by a main loop when something is available in the queue
50
+ def dispatch_message_queue
51
+ while (msg = @message_queue.pop(blocking: false)) # FIXME: EOFError
52
+ process(msg)
53
+ end
54
+ end
55
+
56
+ # Tell a bus to register itself on the glib main loop
57
+ def glibize
58
+ require "glib2"
59
+ # Circumvent a ruby-glib bug
60
+ @channels ||= []
61
+
62
+ gio = GLib::IOChannel.new(@message_queue.socket.fileno)
63
+ @channels << gio
64
+ gio.add_watch(GLib::IOChannel::IN) do |_c, _ch|
65
+ dispatch_message_queue
66
+ true
67
+ end
68
+ end
69
+
70
+ # NAME_FLAG_* and REQUEST_NAME_* belong to BusConnection
71
+ # but users will have referenced them in Connection so they need to stay here
72
+
73
+ # FIXME: describe the following names, flags and constants.
74
+ # See DBus spec for definition
75
+ NAME_FLAG_ALLOW_REPLACEMENT = 0x1
76
+ NAME_FLAG_REPLACE_EXISTING = 0x2
77
+ NAME_FLAG_DO_NOT_QUEUE = 0x4
78
+
79
+ REQUEST_NAME_REPLY_PRIMARY_OWNER = 0x1
80
+ REQUEST_NAME_REPLY_IN_QUEUE = 0x2
81
+ REQUEST_NAME_REPLY_EXISTS = 0x3
82
+ REQUEST_NAME_REPLY_ALREADY_OWNER = 0x4
83
+
84
+ # @api private
85
+ # Send a _message_.
86
+ # If _reply_handler_ is not given, wait for the reply
87
+ # and return the reply, or raise the error.
88
+ # If _reply_handler_ is given, it will be called when the reply
89
+ # eventually arrives, with the reply message as the 1st param
90
+ # and its params following
91
+ def send_sync_or_async(message, &reply_handler)
92
+ ret = nil
93
+ if reply_handler.nil?
94
+ send_sync(message) do |rmsg|
95
+ raise rmsg if rmsg.is_a?(Error)
96
+
97
+ ret = rmsg.params
98
+ end
99
+ else
100
+ on_return(message) do |rmsg|
101
+ if rmsg.is_a?(Error)
102
+ reply_handler.call(rmsg)
103
+ else
104
+ reply_handler.call(rmsg, * rmsg.params)
105
+ end
106
+ end
107
+ @message_queue.push(message)
108
+ end
109
+ ret
110
+ end
111
+
112
+ # @api private
113
+ def introspect_data(dest, path, &reply_handler)
114
+ m = DBus::Message.new(DBus::Message::METHOD_CALL)
115
+ m.path = path
116
+ m.interface = "org.freedesktop.DBus.Introspectable"
117
+ m.destination = dest
118
+ m.member = "Introspect"
119
+ m.sender = unique_name
120
+ if reply_handler.nil?
121
+ send_sync_or_async(m).first
122
+ else
123
+ send_sync_or_async(m) do |*args|
124
+ # TODO: test async introspection, is it used at all?
125
+ args.shift # forget the message, pass only the text
126
+ reply_handler.call(*args)
127
+ nil
128
+ end
129
+ end
130
+ end
131
+
132
+ # @api private
133
+ # Issues a call to the org.freedesktop.DBus.Introspectable.Introspect method
134
+ # _dest_ is the service and _path_ the object path you want to introspect
135
+ # If a code block is given, the introspect call in asynchronous. If not
136
+ # data is returned
137
+ #
138
+ # FIXME: link to ProxyObject data definition
139
+ # The returned object is a ProxyObject that has methods you can call to
140
+ # issue somme METHOD_CALL messages, and to setup to receive METHOD_RETURN
141
+ def introspect(dest, path)
142
+ if !block_given?
143
+ # introspect in synchronous !
144
+ data = introspect_data(dest, path)
145
+ pof = DBus::ProxyObjectFactory.new(data, self, dest, path)
146
+ pof.build
147
+ else
148
+ introspect_data(dest, path) do |async_data|
149
+ yield(DBus::ProxyObjectFactory.new(async_data, self, dest, path).build)
150
+ end
151
+ end
152
+ end
153
+
154
+ # Exception raised when a service name is requested that is not available.
155
+ class NameRequestError < Exception
156
+ # @return [Integer] one of
157
+ # REQUEST_NAME_REPLY_IN_QUEUE
158
+ # REQUEST_NAME_REPLY_EXISTS
159
+ attr_reader :error_code
160
+
161
+ def initialize(error_code)
162
+ @error_code = error_code
163
+ super()
164
+ end
165
+ end
166
+
167
+ # In case RequestName did not succeed, raise an exception but first ask the bus who owns the name instead of us
168
+ # @param ret [Integer] what RequestName returned
169
+ # @param name Name that was requested
170
+ # @return [REQUEST_NAME_REPLY_PRIMARY_OWNER,REQUEST_NAME_REPLY_ALREADY_OWNER] on success
171
+ # @raise [NameRequestError] with #error_code REQUEST_NAME_REPLY_EXISTS or REQUEST_NAME_REPLY_IN_QUEUE, on failure
172
+ # @api private
173
+ def handle_return_of_request_name(ret, name)
174
+ if [REQUEST_NAME_REPLY_EXISTS, REQUEST_NAME_REPLY_IN_QUEUE].include?(ret)
175
+ other = proxy.GetNameOwner(name).first
176
+ other_creds = proxy.GetConnectionCredentials(other).first
177
+ message = "Could not request #{name}, already owned by #{other}, #{other_creds.inspect}"
178
+ raise NameRequestError.new(ret), message
179
+ end
180
+
181
+ ret
182
+ end
183
+
184
+ # Attempt to request a service _name_.
185
+ # @raise NameRequestError which cannot really be rescued as it will be raised when dispatching a later call.
186
+ # @return [ObjectServer]
187
+ # @deprecated Use {BusConnection#request_name}.
188
+ def request_service(name)
189
+ # Use RequestName, but asynchronously!
190
+ # A synchronous call would not work with service activation, where
191
+ # method calls to be serviced arrive before the reply for RequestName
192
+ # (Ticket#29).
193
+ proxy.RequestName(name, NAME_FLAG_REPLACE_EXISTING) do |rmsg, r|
194
+ # check and report errors first
195
+ raise rmsg if rmsg.is_a?(Error)
196
+
197
+ handle_return_of_request_name(r, name)
198
+ end
199
+ object_server
200
+ end
201
+
202
+ # @api private
203
+ # Wait for a message to arrive. Return it once it is available.
204
+ def wait_for_message
205
+ @message_queue.pop # FIXME: EOFError
206
+ end
207
+
208
+ # @api private
209
+ # Send a message _msg_ on to the bus. This is done synchronously, thus
210
+ # the call will block until a reply message arrives.
211
+ # @param msg [Message]
212
+ # @param retc [Proc] the reply handler
213
+ # @yieldparam rmsg [MethodReturnMessage] the reply
214
+ # @yieldreturn [Array<Object>] the reply (out) parameters
215
+ def send_sync(msg, &retc) # :yields: reply/return message
216
+ return if msg.nil? # check if somethings wrong
217
+
218
+ @message_queue.push(msg)
219
+ @method_call_msgs[msg.serial] = msg
220
+ @method_call_replies[msg.serial] = retc
221
+
222
+ retm = wait_for_message
223
+ return if retm.nil? # check if somethings wrong
224
+
225
+ process(retm)
226
+ while @method_call_replies.key? msg.serial
227
+ retm = wait_for_message
228
+ process(retm)
229
+ end
230
+ rescue EOFError
231
+ new_err = DBus::Error.new("Connection dropped after we sent #{msg.inspect}")
232
+ raise new_err
233
+ end
234
+
235
+ # @api private
236
+ # Specify a code block that has to be executed when a reply for
237
+ # message _msg_ is received.
238
+ # @param msg [Message]
239
+ def on_return(msg, &retc)
240
+ # Have a better exception here
241
+ if msg.message_type != Message::METHOD_CALL
242
+ raise "on_return should only get method_calls"
243
+ end
244
+
245
+ @method_call_msgs[msg.serial] = msg
246
+ @method_call_replies[msg.serial] = retc
247
+ end
248
+
249
+ # Asks bus to send us messages matching mr, and execute slot when
250
+ # received
251
+ # @param match_rule [MatchRule,#to_s]
252
+ # @return [void] actually return whether the rule existed, internal detail
253
+ def add_match(match_rule, &slot)
254
+ # check this is a signal.
255
+ mrs = match_rule.to_s
256
+ DBus.logger.debug "#{@signal_matchrules.size} rules, adding #{mrs.inspect}"
257
+ rule_existed = @signal_matchrules.key?(mrs)
258
+ @signal_matchrules[mrs] = slot
259
+ rule_existed
260
+ end
261
+
262
+ # @param match_rule [MatchRule,#to_s]
263
+ # @return [void] actually return whether the rule existed, internal detail
264
+ def remove_match(match_rule)
265
+ mrs = match_rule.to_s
266
+ @signal_matchrules.delete(mrs).nil?
267
+ end
268
+
269
+ # @api private
270
+ # Process a message _msg_ based on its type.
271
+ # @param msg [Message]
272
+ def process(msg)
273
+ return if msg.nil? # check if somethings wrong
274
+
275
+ case msg.message_type
276
+ when Message::ERROR, Message::METHOD_RETURN
277
+ raise InvalidPacketException if msg.reply_serial.nil?
278
+
279
+ mcs = @method_call_replies[msg.reply_serial]
280
+ if !mcs
281
+ DBus.logger.debug "no return code for mcs: #{mcs.inspect} msg: #{msg.inspect}"
282
+ else
283
+ if msg.message_type == Message::ERROR
284
+ mcs.call(Error.new(msg))
285
+ else
286
+ mcs.call(msg)
287
+ end
288
+ @method_call_replies.delete(msg.reply_serial)
289
+ @method_call_msgs.delete(msg.reply_serial)
290
+ end
291
+ when DBus::Message::METHOD_CALL
292
+ if msg.path == "/org/freedesktop/DBus"
293
+ DBus.logger.debug "Got method call on /org/freedesktop/DBus"
294
+ end
295
+ node = object_server.get_node(msg.path, create: false)
296
+ # introspect a known path even if there is no object on it
297
+ if node &&
298
+ msg.interface == "org.freedesktop.DBus.Introspectable" &&
299
+ msg.member == "Introspect"
300
+ reply = Message.new(Message::METHOD_RETURN).reply_to(msg)
301
+ reply.sender = @unique_name
302
+ xml = node.to_xml(msg.path)
303
+ reply.add_param(Type::STRING, xml)
304
+ @message_queue.push(reply)
305
+ # dispatch for an object
306
+ elsif node&.object
307
+ node.object.dispatch(msg)
308
+ else
309
+ reply = Message.error(msg, "org.freedesktop.DBus.Error.UnknownObject",
310
+ "Object #{msg.path} doesn't exist")
311
+ @message_queue.push(reply)
312
+ end
313
+ when DBus::Message::SIGNAL
314
+ # the signal can match multiple different rules
315
+ # clone to allow new signale handlers to be registered
316
+ @signal_matchrules.dup.each do |mrs, slot|
317
+ if DBus::MatchRule.new.from_s(mrs).match(msg)
318
+ slot.call(msg)
319
+ end
320
+ end
321
+ else
322
+ # spec(Message Format): Unknown types must be ignored.
323
+ DBus.logger.debug "Unknown message type: #{msg.message_type}"
324
+ end
325
+ rescue Exception => e
326
+ raise msg.annotate_exception(e)
327
+ end
328
+
329
+ # @api private
330
+ # Emit a signal event for the given _service_, object _obj_, interface
331
+ # _intf_ and signal _sig_ with arguments _args_.
332
+ # @param _service unused
333
+ # @param obj [DBus::Object]
334
+ # @param intf [Interface]
335
+ # @param sig [Signal]
336
+ # @param args arguments for the signal
337
+ def emit(_service, obj, intf, sig, *args)
338
+ m = Message.new(DBus::Message::SIGNAL)
339
+ m.path = obj.path
340
+ m.interface = intf.name
341
+ m.member = sig.name
342
+ i = 0
343
+ sig.params.each do |par|
344
+ m.add_param(par.type, args[i])
345
+ i += 1
346
+ end
347
+ @message_queue.push(m)
348
+ end
349
+ end
350
+
351
+ # A {Connection} that is talking directly to a peer, with no bus daemon in between.
352
+ # A prominent example is the PulseAudio connection,
353
+ # see https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/DBus/
354
+ # When starting, it still starts with authentication but omits the Hello message.
355
+ class PeerConnection < Connection
356
+ # Get a {ProxyPeerService}, a dummy helper to get {ProxyObject}s for
357
+ # a {PeerConnection}.
358
+ # @return [ProxyPeerService]
359
+ def peer_service
360
+ ProxyPeerService.new(self)
361
+ end
362
+ end
363
+ end
@@ -2,7 +2,7 @@
2
2
  # copied from activesupport/core_ext from Rails, MIT license
3
3
  # https://github.com/rails/rails/tree/9794e85351243cac6d4e78adaba634b8e4ecad0a/activesupport/lib/active_support/core_ext
4
4
 
5
- require "dbus/core_ext/module/redefine_method"
5
+ require_relative "../module/redefine_method"
6
6
 
7
7
  class Class
8
8
  # Declare a class-level attribute whose value is inheritable by subclasses.