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.
- checksums.yaml +4 -4
- data/NEWS.md +38 -3
- data/VERSION +1 -1
- data/doc/Reference.md +1 -1
- data/examples/no-bus/pulseaudio.rb +50 -0
- data/examples/utils/listnames.rb +9 -1
- data/lib/dbus/bus.rb +125 -432
- data/lib/dbus/connection.rb +363 -0
- data/lib/dbus/object.rb +46 -18
- data/lib/dbus/object_manager.rb +5 -1
- data/lib/dbus/object_server.rb +44 -14
- data/lib/dbus/org.freedesktop.DBus.xml +97 -0
- data/lib/dbus/proxy_service.rb +14 -2
- data/lib/dbus.rb +1 -0
- data/ruby-dbus.gemspec +1 -1
- data/spec/bus_connection_spec.rb +27 -15
- data/spec/connection_spec.rb +37 -0
- data/spec/dbus_spec.rb +22 -0
- data/spec/mock-service/spaghetti-monster.rb +4 -5
- data/spec/object_server_spec.rb +58 -24
- data/spec/object_spec.rb +16 -0
- data/spec/proxy_service_spec.rb +12 -7
- data/spec/tools/dbus-limited-session.conf +3 -0
- metadata +10 -5
@@ -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
|
-
|
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
|
43
|
-
|
44
|
-
|
45
|
-
#
|
46
|
-
@service
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
209
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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_.
|
data/lib/dbus/object_manager.rb
CHANGED
@@ -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 =
|
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
|
data/lib/dbus/object_server.rb
CHANGED
@@ -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
|
-
|
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.
|
55
|
+
obj.object_server = self
|
56
56
|
object_manager_for(obj)&.object_added(obj)
|
57
57
|
end
|
58
58
|
|
59
|
-
# Undo exporting an object
|
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
|
63
|
-
def unexport(
|
64
|
-
|
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.
|
75
|
-
|
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
|