ruby-dbus 0.23.0.beta1 → 0.23.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
data/lib/dbus/object.rb CHANGED
@@ -19,31 +19,37 @@ 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
- # @return [Connection] the connection the object is exported by
30
- attr_reader :connection
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
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
- @connection = 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
40
45
  end
41
46
 
42
- # @param connection [Connection] the connection the object is exported by
43
- def connection=(connection)
44
- @connection = connection
45
- # deprecated, keeping @service for compatibility
46
- @service = connection&.object_server
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
47
53
  end
48
54
 
49
55
  # Dispatch a message _msg_ to call exported methods
@@ -78,7 +84,9 @@ module DBus
78
84
  dbus_msg_exc = msg.annotate_exception(e)
79
85
  reply = ErrorMessage.from_exception(dbus_msg_exc).reply_to(msg)
80
86
  end
81
- @connection.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)
82
90
  end
83
91
  end
84
92
 
@@ -148,18 +156,32 @@ module DBus
148
156
  dbus_accessor(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
149
157
  end
150
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
+
151
170
  # A read-only property accessing an instance variable.
152
171
  # A combination of `attr_reader` and {.dbus_reader}.
153
172
  #
173
+ # You may be instead looking for a variant which is read-write from the Ruby side:
174
+ # {.dbus_reader_attr_accessor}.
175
+ #
154
176
  # Whenever the property value gets changed from "inside" the object,
155
177
  # you should emit the `PropertiesChanged` signal by calling
156
178
  # {#dbus_properties_changed}.
157
179
  #
158
- # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
180
+ # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
159
181
  #
160
182
  # or, omitting the value in the signal,
161
183
  #
162
- # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
184
+ # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
163
185
  #
164
186
  # @param (see .dbus_attr_accessor)
165
187
  # @return (see .dbus_attr_accessor)
@@ -205,18 +227,22 @@ module DBus
205
227
  # implement it with a read-write attr_accessor. In that case this method
206
228
  # uses {.dbus_watcher} to set up the PropertiesChanged signal.
207
229
  #
208
- # attr_accessor :foo_bar
209
- # dbus_reader :foo_bar, "s"
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"
210
236
  #
211
237
  # If the property value should change by other means than its attr_writer,
212
238
  # you should emit the `PropertiesChanged` signal by calling
213
239
  # {#dbus_properties_changed}.
214
240
  #
215
- # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
241
+ # dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
216
242
  #
217
243
  # or, omitting the value in the signal,
218
244
  #
219
- # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
245
+ # dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
220
246
  #
221
247
  # @param (see .dbus_attr_accessor)
222
248
  # @return (see .dbus_attr_accessor)
@@ -316,7 +342,9 @@ module DBus
316
342
  # @param sig [Signal]
317
343
  # @param args arguments for the signal
318
344
  def emit(intf, sig, *args)
319
- @connection.emit(nil, 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)
320
348
  end
321
349
 
322
350
  # Defines a signal for the object with a given name _sym_ and _prototype_.
@@ -20,27 +20,31 @@ module DBus
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
- descendant_objects = connection.object_server.descendants_for(path)
27
+ descendant_objects = object_server.descendants_for(path)
27
28
  descendant_objects.each_with_object({}) do |obj, hash|
28
29
  hash[obj.path] = obj.interfaces_and_properties
29
30
  end
30
31
  end
31
32
 
33
+ # {ObjectServer#export} will call this for you to emit the `InterfacesAdded` signal.
32
34
  # @param object [DBus::Object]
33
35
  # @return [void]
34
36
  def object_added(object)
35
37
  InterfacesAdded(object.path, object.interfaces_and_properties)
36
38
  end
37
39
 
40
+ # {ObjectServer#unexport} will call this for you to emit the `InterfacesRemoved` signal.
38
41
  # @param object [DBus::Object]
39
42
  # @return [void]
40
43
  def object_removed(object)
41
44
  InterfacesRemoved(object.path, object.intfs.keys)
42
45
  end
43
46
 
47
+ # Module#included, a hook for `include ObjectManager`, declares its dbus_interface.
44
48
  def self.included(base)
45
49
  base.class_eval do
46
50
  dbus_interface OBJECT_MANAGER_INTERFACE do
@@ -36,7 +36,7 @@ module DBus
36
36
 
37
37
  # Retrieves an object at the given _path_
38
38
  # @param path [ObjectPath]
39
- # @return [DBus::Object]
39
+ # @return [DBus::Object,nil]
40
40
  def object(path)
41
41
  node = get_node(path, create: false)
42
42
  node&.object
@@ -45,34 +45,48 @@ module DBus
45
45
 
46
46
  # Export an object
47
47
  # @param obj [DBus::Object]
48
+ # @raise RuntimeError if there's already an exported object at the same path
48
49
  def export(obj)
49
50
  node = get_node(obj.path, create: true)
50
- # TODO: clarify that this is indeed the right thing, and document
51
- # raise "At #{obj.path} there is already an object #{node.object.inspect}" if node.object
51
+ raise "At #{obj.path} there is already an object #{node.object.inspect}" if node.object
52
52
 
53
53
  node.object = obj
54
54
 
55
- obj.connection = @connection
55
+ obj.object_server = self
56
56
  object_manager_for(obj)&.object_added(obj)
57
57
  end
58
58
 
59
- # Undo exporting an object _obj_.
59
+ # Undo exporting an object *obj_or_path*.
60
60
  # Raises ArgumentError if it is not a DBus::Object.
61
61
  # Returns the object, or false if _obj_ was not exported.
62
- # @param obj [DBus::Object]
63
- def unexport(obj)
64
- raise ArgumentError, "Expecting a DBus::Object argument" unless obj.is_a?(DBus::Object)
65
-
66
- last_path_separator_idx = obj.path.rindex("/")
67
- parent_path = obj.path[1..last_path_separator_idx - 1]
68
- node_name = obj.path[last_path_separator_idx + 1..-1]
62
+ # @param obj_or_path [DBus::Object,ObjectPath,String] an object or a valid object path
63
+ def unexport(obj_or_path)
64
+ path = self.class.path_of(obj_or_path)
65
+ parent_path, _separator, node_name = path.rpartition("/")
69
66
 
70
67
  parent_node = get_node(parent_path, create: false)
71
68
  return false unless parent_node
72
69
 
70
+ node = if node_name == "" # path == "/"
71
+ parent_node
72
+ else
73
+ parent_node[node_name]
74
+ end
75
+ obj = node&.object
76
+ raise ArgumentError, "Cannot unexport, no object at #{path}" unless obj
77
+
73
78
  object_manager_for(obj)&.object_removed(obj)
74
- obj.connection = nil
75
- parent_node.delete(node_name).object
79
+ obj.object_server = nil
80
+ node.object = nil
81
+
82
+ # node can be deleted if
83
+ # - it has no children
84
+ # - it is not root
85
+ if node.empty? && !node.equal?(parent_node)
86
+ parent_node.delete(node_name)
87
+ end
88
+
89
+ obj
76
90
  end
77
91
 
78
92
  # Find the (closest) parent of *object*
@@ -98,6 +112,22 @@ module DBus
98
112
  node.descendant_objects
99
113
  end
100
114
 
115
+ # @param obj_or_path [DBus::Object,ObjectPath,String] an object or a valid object path
116
+ # @return [ObjectPath]
117
+ # @api private
118
+ def self.path_of(obj_or_path)
119
+ case obj_or_path
120
+ when ObjectPath
121
+ obj_or_path
122
+ when String
123
+ ObjectPath.new(obj_or_path)
124
+ when DBus::Object
125
+ obj_or_path.path
126
+ else
127
+ raise ArgumentError, "Expecting a DBus::Object argument or DBus::ObjectPath or String which parses as one"
128
+ end
129
+ end
130
+
101
131
  #########
102
132
 
103
133
  private