ruby-dbus 0.22.1 → 0.23.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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