ruby-dbus 0.22.1 → 0.23.0.beta2

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS.md +38 -0
  3. data/VERSION +1 -1
  4. data/doc/Reference.md +5 -5
  5. data/examples/no-bus/pulseaudio.rb +50 -0
  6. data/examples/service/complex-property.rb +2 -2
  7. data/examples/service/service_newapi.rb +2 -2
  8. data/lib/dbus/bus.rb +48 -694
  9. data/lib/dbus/connection.rb +350 -0
  10. data/lib/dbus/logger.rb +3 -2
  11. data/lib/dbus/main.rb +66 -0
  12. data/lib/dbus/message.rb +6 -8
  13. data/lib/dbus/node_tree.rb +105 -0
  14. data/lib/dbus/object.rb +28 -9
  15. data/lib/dbus/object_manager.rb +6 -3
  16. data/lib/dbus/object_server.rb +149 -0
  17. data/lib/dbus/org.freedesktop.DBus.xml +97 -0
  18. data/lib/dbus/proxy_object.rb +4 -4
  19. data/lib/dbus/proxy_service.rb +107 -0
  20. data/lib/dbus.rb +10 -8
  21. data/ruby-dbus.gemspec +1 -1
  22. data/spec/bus_connection_spec.rb +80 -0
  23. data/spec/connection_spec.rb +37 -0
  24. data/spec/coverage_helper.rb +39 -0
  25. data/spec/dbus_spec.rb +22 -0
  26. data/spec/main_loop_spec.rb +14 -0
  27. data/spec/message_spec.rb +21 -0
  28. data/spec/mock-service/cockpit-dbustests.rb +29 -0
  29. data/spec/mock-service/com.redhat.Cockpit.DBusTests.xml +180 -0
  30. data/spec/mock-service/org.ruby.service.service +4 -0
  31. data/spec/mock-service/org.rubygems.ruby_dbus.DBusTests.service +4 -0
  32. data/spec/{service_newapi.rb → mock-service/spaghetti-monster.rb} +21 -10
  33. data/spec/node_spec.rb +1 -5
  34. data/spec/object_server_spec.rb +138 -0
  35. data/spec/object_spec.rb +46 -0
  36. data/spec/{bus_driver_spec.rb → proxy_service_spec.rb} +13 -8
  37. data/spec/spec_helper.rb +9 -45
  38. data/spec/thread_safety_spec.rb +9 -11
  39. data/spec/tools/dbus-launch-simple +4 -1
  40. data/spec/tools/dbus-limited-session.conf +3 -0
  41. data/spec/tools/test_env +26 -6
  42. metadata +24 -10
  43. data/spec/server_spec.rb +0 -55
  44. data/spec/service_spec.rb +0 -18
  45. data/spec/tools/test_server +0 -39
@@ -0,0 +1,350 @@
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
+ end
157
+
158
+ def handle_return_of_request_name(ret, name)
159
+ details = if ret == REQUEST_NAME_REPLY_IN_QUEUE
160
+ other = proxy.GetNameOwner(name).first
161
+ other_creds = proxy.GetConnectionCredentials(other).first
162
+ "already owned by #{other}, #{other_creds.inspect}"
163
+ else
164
+ "error code #{ret}"
165
+ end
166
+ raise NameRequestError, "Could not request #{name}, #{details}" unless ret == REQUEST_NAME_REPLY_PRIMARY_OWNER
167
+
168
+ ret
169
+ end
170
+
171
+ # Attempt to request a service _name_.
172
+ # @raise NameRequestError which cannot really be rescued as it will be raised when dispatching a later call.
173
+ # @return [ObjectServer]
174
+ # @deprecated Use {BusConnection#request_name}.
175
+ def request_service(name)
176
+ # Use RequestName, but asynchronously!
177
+ # A synchronous call would not work with service activation, where
178
+ # method calls to be serviced arrive before the reply for RequestName
179
+ # (Ticket#29).
180
+ proxy.RequestName(name, NAME_FLAG_REPLACE_EXISTING) do |rmsg, r|
181
+ # check and report errors first
182
+ raise rmsg if rmsg.is_a?(Error)
183
+
184
+ handle_return_of_request_name(r, name)
185
+ end
186
+ object_server
187
+ end
188
+
189
+ # @api private
190
+ # Wait for a message to arrive. Return it once it is available.
191
+ def wait_for_message
192
+ @message_queue.pop # FIXME: EOFError
193
+ end
194
+
195
+ # @api private
196
+ # Send a message _msg_ on to the bus. This is done synchronously, thus
197
+ # the call will block until a reply message arrives.
198
+ # @param msg [Message]
199
+ # @param retc [Proc] the reply handler
200
+ # @yieldparam rmsg [MethodReturnMessage] the reply
201
+ # @yieldreturn [Array<Object>] the reply (out) parameters
202
+ def send_sync(msg, &retc) # :yields: reply/return message
203
+ return if msg.nil? # check if somethings wrong
204
+
205
+ @message_queue.push(msg)
206
+ @method_call_msgs[msg.serial] = msg
207
+ @method_call_replies[msg.serial] = retc
208
+
209
+ retm = wait_for_message
210
+ return if retm.nil? # check if somethings wrong
211
+
212
+ process(retm)
213
+ while @method_call_replies.key? msg.serial
214
+ retm = wait_for_message
215
+ process(retm)
216
+ end
217
+ rescue EOFError
218
+ new_err = DBus::Error.new("Connection dropped after we sent #{msg.inspect}")
219
+ raise new_err
220
+ end
221
+
222
+ # @api private
223
+ # Specify a code block that has to be executed when a reply for
224
+ # message _msg_ is received.
225
+ # @param msg [Message]
226
+ def on_return(msg, &retc)
227
+ # Have a better exception here
228
+ if msg.message_type != Message::METHOD_CALL
229
+ raise "on_return should only get method_calls"
230
+ end
231
+
232
+ @method_call_msgs[msg.serial] = msg
233
+ @method_call_replies[msg.serial] = retc
234
+ end
235
+
236
+ # Asks bus to send us messages matching mr, and execute slot when
237
+ # received
238
+ # @param match_rule [MatchRule,#to_s]
239
+ # @return [void] actually return whether the rule existed, internal detail
240
+ def add_match(match_rule, &slot)
241
+ # check this is a signal.
242
+ mrs = match_rule.to_s
243
+ DBus.logger.debug "#{@signal_matchrules.size} rules, adding #{mrs.inspect}"
244
+ rule_existed = @signal_matchrules.key?(mrs)
245
+ @signal_matchrules[mrs] = slot
246
+ rule_existed
247
+ end
248
+
249
+ # @param match_rule [MatchRule,#to_s]
250
+ # @return [void] actually return whether the rule existed, internal detail
251
+ def remove_match(match_rule)
252
+ mrs = match_rule.to_s
253
+ @signal_matchrules.delete(mrs).nil?
254
+ end
255
+
256
+ # @api private
257
+ # Process a message _msg_ based on its type.
258
+ # @param msg [Message]
259
+ def process(msg)
260
+ return if msg.nil? # check if somethings wrong
261
+
262
+ case msg.message_type
263
+ when Message::ERROR, Message::METHOD_RETURN
264
+ raise InvalidPacketException if msg.reply_serial.nil?
265
+
266
+ mcs = @method_call_replies[msg.reply_serial]
267
+ if !mcs
268
+ DBus.logger.debug "no return code for mcs: #{mcs.inspect} msg: #{msg.inspect}"
269
+ else
270
+ if msg.message_type == Message::ERROR
271
+ mcs.call(Error.new(msg))
272
+ else
273
+ mcs.call(msg)
274
+ end
275
+ @method_call_replies.delete(msg.reply_serial)
276
+ @method_call_msgs.delete(msg.reply_serial)
277
+ end
278
+ when DBus::Message::METHOD_CALL
279
+ if msg.path == "/org/freedesktop/DBus"
280
+ DBus.logger.debug "Got method call on /org/freedesktop/DBus"
281
+ end
282
+ node = object_server.get_node(msg.path, create: false)
283
+ # introspect a known path even if there is no object on it
284
+ if node &&
285
+ msg.interface == "org.freedesktop.DBus.Introspectable" &&
286
+ msg.member == "Introspect"
287
+ reply = Message.new(Message::METHOD_RETURN).reply_to(msg)
288
+ reply.sender = @unique_name
289
+ xml = node.to_xml(msg.path)
290
+ reply.add_param(Type::STRING, xml)
291
+ @message_queue.push(reply)
292
+ # dispatch for an object
293
+ elsif node&.object
294
+ node.object.dispatch(msg)
295
+ else
296
+ reply = Message.error(msg, "org.freedesktop.DBus.Error.UnknownObject",
297
+ "Object #{msg.path} doesn't exist")
298
+ @message_queue.push(reply)
299
+ end
300
+ when DBus::Message::SIGNAL
301
+ # the signal can match multiple different rules
302
+ # clone to allow new signale handlers to be registered
303
+ @signal_matchrules.dup.each do |mrs, slot|
304
+ if DBus::MatchRule.new.from_s(mrs).match(msg)
305
+ slot.call(msg)
306
+ end
307
+ end
308
+ else
309
+ # spec(Message Format): Unknown types must be ignored.
310
+ DBus.logger.debug "Unknown message type: #{msg.message_type}"
311
+ end
312
+ rescue Exception => e
313
+ raise msg.annotate_exception(e)
314
+ end
315
+
316
+ # @api private
317
+ # Emit a signal event for the given _service_, object _obj_, interface
318
+ # _intf_ and signal _sig_ with arguments _args_.
319
+ # @param _service unused
320
+ # @param obj [DBus::Object]
321
+ # @param intf [Interface]
322
+ # @param sig [Signal]
323
+ # @param args arguments for the signal
324
+ def emit(_service, obj, intf, sig, *args)
325
+ m = Message.new(DBus::Message::SIGNAL)
326
+ m.path = obj.path
327
+ m.interface = intf.name
328
+ m.member = sig.name
329
+ i = 0
330
+ sig.params.each do |par|
331
+ m.add_param(par.type, args[i])
332
+ i += 1
333
+ end
334
+ @message_queue.push(m)
335
+ end
336
+ end
337
+
338
+ # A {Connection} that is talking directly to a peer, with no bus daemon in between.
339
+ # A prominent example is the PulseAudio connection,
340
+ # see https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/DBus/
341
+ # When starting, it still starts with authentication but omits the Hello message.
342
+ class PeerConnection < Connection
343
+ # Get a {ProxyPeerService}, a dummy helper to get {ProxyObject}s for
344
+ # a {PeerConnection}.
345
+ # @return [ProxyPeerService]
346
+ def peer_service
347
+ ProxyPeerService.new(self)
348
+ end
349
+ end
350
+ end
data/lib/dbus/logger.rb CHANGED
@@ -17,9 +17,10 @@ module DBus
17
17
  # The default one logs to STDERR,
18
18
  # with DEBUG if $DEBUG is set, otherwise INFO.
19
19
  def logger
20
- unless defined? @logger
20
+ if @logger.nil?
21
+ debug = $DEBUG || ENV["RUBY_DBUS_DEBUG"]
21
22
  @logger = Logger.new($stderr)
22
- @logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
23
+ @logger.level = debug ? Logger::DEBUG : Logger::INFO
23
24
  end
24
25
  @logger
25
26
  end
data/lib/dbus/main.rb ADDED
@@ -0,0 +1,66 @@
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
+ # = Main event loop class.
14
+ #
15
+ # Class that takes care of handling message and signal events
16
+ # asynchronously. *Note:* This is a native implement and therefore does
17
+ # not integrate with a graphical widget set main loop.
18
+ class Main
19
+ # Create a new main event loop.
20
+ def initialize
21
+ @buses = {}
22
+ @quitting = false
23
+ end
24
+
25
+ # Add a _bus_ to the list of buses to watch for events.
26
+ def <<(bus)
27
+ @buses[bus.message_queue.socket] = bus
28
+ end
29
+
30
+ # Quit a running main loop, to be used eg. from a signal handler
31
+ def quit
32
+ @quitting = true
33
+ end
34
+
35
+ # Run the main loop. This is a blocking call!
36
+ def run
37
+ # before blocking, empty the buffers
38
+ # https://bugzilla.novell.com/show_bug.cgi?id=537401
39
+ @buses.each_value do |b|
40
+ while (m = b.message_queue.message_from_buffer_nonblock)
41
+ b.process(m)
42
+ end
43
+ end
44
+ while !@quitting && !@buses.empty?
45
+ ready = IO.select(@buses.keys, [], [], 5) # timeout 5 seconds
46
+ next unless ready # timeout exceeds so continue unless quitting
47
+
48
+ ready.first.each do |socket|
49
+ b = @buses[socket]
50
+ begin
51
+ b.message_queue.buffer_from_socket_nonblock
52
+ rescue EOFError, SystemCallError => e
53
+ DBus.logger.debug "Got #{e.inspect} from #{socket.inspect}"
54
+ @buses.delete socket # this bus died
55
+ next
56
+ end
57
+ while (m = b.message_queue.message_from_buffer_nonblock)
58
+ b.process(m)
59
+ end
60
+ end
61
+ end
62
+ DBus.logger.debug "Main loop quit" if @quitting
63
+ DBus.logger.debug "Main loop quit, no connections left" if @buses.empty?
64
+ end
65
+ end
66
+ end
data/lib/dbus/message.rb CHANGED
@@ -16,12 +16,6 @@ require_relative "raw_message"
16
16
  #
17
17
  # Module containing all the D-Bus modules and classes.
18
18
  module DBus
19
- # = InvalidDestinationName class
20
- # Thrown when you try to send a message to /org/freedesktop/DBus/Local, that
21
- # is reserved.
22
- class InvalidDestinationName < Exception
23
- end
24
-
25
19
  # = D-Bus message class
26
20
  #
27
21
  # Class that holds any type of message that travels over the bus.
@@ -164,11 +158,15 @@ module DBus
164
158
  SENDER = 7
165
159
  SIGNATURE = 8
166
160
 
161
+ RESERVED_PATH = "/org/freedesktop/DBus/Local"
162
+
167
163
  # Marshall the message with its current set parameters and return
168
164
  # it in a packet form.
165
+ # @return [String]
169
166
  def marshall
170
- if @path == "/org/freedesktop/DBus/Local"
171
- raise InvalidDestinationName
167
+ if @path == RESERVED_PATH
168
+ # the bus would disconnect us, better explain why
169
+ raise "Cannot send a message with the reserved path #{RESERVED_PATH}: #{inspect}"
172
170
  end
173
171
 
174
172
  params_marshaller = PacketMarshaller.new(endianness: ENDIANNESS)
@@ -0,0 +1,105 @@
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
+ # Has a tree of {Node}s, refering to {Object}s or to {ProxyObject}s.
14
+ class NodeTree
15
+ # @return [Node]
16
+ attr_reader :root
17
+
18
+ def initialize
19
+ @root = Node.new("/")
20
+ end
21
+
22
+ # Get the object node corresponding to the given *path*.
23
+ # @param path [ObjectPath]
24
+ # @param create [Boolean] if true, the the {Node}s in the path are created
25
+ # if they do not already exist.
26
+ # @return [Node,nil]
27
+ def get_node(path, create: false)
28
+ n = @root
29
+ path.sub(%r{^/}, "").split("/").each do |elem|
30
+ if !(n[elem])
31
+ return nil if !create
32
+
33
+ n[elem] = Node.new(elem)
34
+ end
35
+ n = n[elem]
36
+ end
37
+ n
38
+ end
39
+ end
40
+
41
+ # = Object path node class
42
+ #
43
+ # Class representing a node on an object path.
44
+ class Node < Hash
45
+ # @return [DBus::Object,DBus::ProxyObject,nil]
46
+ # The D-Bus object contained by the node.
47
+ attr_accessor :object
48
+
49
+ # The name of the node.
50
+ # @return [String] the last component of its object path, or "/"
51
+ attr_reader :name
52
+
53
+ # Create a new node with a given _name_.
54
+ def initialize(name)
55
+ super()
56
+ @name = name
57
+ @object = nil
58
+ end
59
+
60
+ # Return an XML string representation of the node.
61
+ # It is shallow, not recursing into subnodes
62
+ # @param node_opath [String]
63
+ def to_xml(node_opath)
64
+ xml = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
65
+ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
66
+ '
67
+ xml += "<node name=\"#{node_opath}\">\n"
68
+ each_key do |k|
69
+ xml += " <node name=\"#{k}\" />\n"
70
+ end
71
+ @object&.intfs&.each_value do |v|
72
+ xml += v.to_xml
73
+ end
74
+ xml += "</node>"
75
+ xml
76
+ end
77
+
78
+ # Return inspect information of the node.
79
+ def inspect
80
+ # Need something here
81
+ "<DBus::Node #{sub_inspect}>"
82
+ end
83
+
84
+ # Return instance inspect information, used by Node#inspect.
85
+ def sub_inspect
86
+ s = ""
87
+ if !@object.nil?
88
+ s += format("%x ", @object.object_id)
89
+ end
90
+ contents_sub_inspect = keys
91
+ .map { |k| "#{k} => #{self[k].sub_inspect}" }
92
+ .join(",")
93
+ "#{s}{#{contents_sub_inspect}}"
94
+ end
95
+
96
+ # All objects (not paths) under this path (except itself).
97
+ # @return [Array<DBus::Object>]
98
+ def descendant_objects
99
+ children_objects = values.map(&:object).compact
100
+ descendants = values.map(&:descendant_objects)
101
+ flat_descendants = descendants.reduce([], &:+)
102
+ children_objects + flat_descendants
103
+ end
104
+ end
105
+ end
data/lib/dbus/object.rb CHANGED
@@ -19,27 +19,42 @@ module DBus
19
19
  # Objects that are going to be exported by a D-Bus service
20
20
  # should inherit from this class. At the client side, use {ProxyObject}.
21
21
  class Object
22
- # The path of the object.
22
+ # @return [ObjectPath] The path of the object.
23
23
  attr_reader :path
24
24
 
25
25
  # The interfaces that the object supports. Hash: String => Interface
26
26
  my_class_attribute :intfs
27
27
  self.intfs = {}
28
28
 
29
- # The service that the object is exported by.
30
- attr_writer :service
31
-
32
29
  @@cur_intf = nil # Interface
33
30
  @@intfs_mutex = Mutex.new
34
31
 
35
32
  # Create a new object with a given _path_.
36
- # Use Service#export to export it.
33
+ # Use ObjectServer#export to export it.
34
+ # @param path [ObjectPath] The path of the object.
37
35
  def initialize(path)
38
36
  @path = path
39
- @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
40
53
  end
41
54
 
42
55
  # Dispatch a message _msg_ to call exported methods
56
+ # @param msg [Message] only METHOD_CALLS do something
57
+ # @api private
43
58
  def dispatch(msg)
44
59
  case msg.message_type
45
60
  when Message::METHOD_CALL
@@ -69,7 +84,9 @@ module DBus
69
84
  dbus_msg_exc = msg.annotate_exception(e)
70
85
  reply = ErrorMessage.from_exception(dbus_msg_exc).reply_to(msg)
71
86
  end
72
- @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)
73
90
  end
74
91
  end
75
92
 
@@ -307,7 +324,9 @@ module DBus
307
324
  # @param sig [Signal]
308
325
  # @param args arguments for the signal
309
326
  def emit(intf, sig, *args)
310
- @service.bus.emit(@service, self, intf, sig, *args)
327
+ raise "Cannot emit signal #{intf.name}.#{sig.name} before #{path} is exported" if object_server.nil?
328
+
329
+ object_server.connection.emit(nil, self, intf, sig, *args)
311
330
  end
312
331
 
313
332
  # Defines a signal for the object with a given name _sym_ and _prototype_.
@@ -316,7 +335,7 @@ module DBus
316
335
 
317
336
  cur_intf = @@cur_intf
318
337
  signal = Signal.new(sym.to_s).from_prototype(prototype)
319
- cur_intf.define(Signal.new(sym.to_s).from_prototype(prototype))
338
+ cur_intf.define(signal)
320
339
 
321
340
  # ::Module#define_method(name) { body }
322
341
  define_method(sym.to_s) do |*args|
@@ -14,34 +14,37 @@ module DBus
14
14
  # {https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
15
15
  # org.freedesktop.DBus.ObjectManager}.
16
16
  #
17
- # {Service#export} and {Service#unexport} will look for an ObjectManager
17
+ # {ObjectServer#export} and {ObjectServer#unexport} will look for an ObjectManager
18
18
  # parent in the path hierarchy. If found, it will emit InterfacesAdded
19
19
  # or InterfacesRemoved, as appropriate.
20
20
  module ObjectManager
21
21
  OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"
22
22
 
23
+ # Implements `the GetManagedObjects` method.
23
24
  # @return [Hash{ObjectPath => Hash{String => Hash{String => Data::Base}}}]
24
25
  # object -> interface -> property -> value
25
26
  def managed_objects
26
- # FIXME: also fix the "service" concept
27
- descendant_objects = @service.descendants_for(path)
27
+ descendant_objects = object_server.descendants_for(path)
28
28
  descendant_objects.each_with_object({}) do |obj, hash|
29
29
  hash[obj.path] = obj.interfaces_and_properties
30
30
  end
31
31
  end
32
32
 
33
+ # {ObjectServer#export} will call this for you to emit the `InterfacesAdded` signal.
33
34
  # @param object [DBus::Object]
34
35
  # @return [void]
35
36
  def object_added(object)
36
37
  InterfacesAdded(object.path, object.interfaces_and_properties)
37
38
  end
38
39
 
40
+ # {ObjectServer#unexport} will call this for you to emit the `InterfacesRemoved` signal.
39
41
  # @param object [DBus::Object]
40
42
  # @return [void]
41
43
  def object_removed(object)
42
44
  InterfacesRemoved(object.path, object.intfs.keys)
43
45
  end
44
46
 
47
+ # Module#included, a hook for `include ObjectManager`, declares its dbus_interface.
45
48
  def self.included(base)
46
49
  base.class_eval do
47
50
  dbus_interface OBJECT_MANAGER_INTERFACE do