ruby-dbus 0.23.0.beta1 → 0.23.1

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.
@@ -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