ruby-dbus 0.22.1 → 0.23.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/NEWS.md +38 -0
- data/VERSION +1 -1
- data/doc/Reference.md +5 -5
- data/examples/no-bus/pulseaudio.rb +50 -0
- data/examples/service/complex-property.rb +2 -2
- data/examples/service/service_newapi.rb +2 -2
- data/lib/dbus/bus.rb +48 -694
- data/lib/dbus/connection.rb +350 -0
- data/lib/dbus/logger.rb +3 -2
- data/lib/dbus/main.rb +66 -0
- data/lib/dbus/message.rb +6 -8
- data/lib/dbus/node_tree.rb +105 -0
- data/lib/dbus/object.rb +28 -9
- data/lib/dbus/object_manager.rb +6 -3
- data/lib/dbus/object_server.rb +149 -0
- data/lib/dbus/org.freedesktop.DBus.xml +97 -0
- data/lib/dbus/proxy_object.rb +4 -4
- data/lib/dbus/proxy_service.rb +107 -0
- data/lib/dbus.rb +10 -8
- data/ruby-dbus.gemspec +1 -1
- data/spec/bus_connection_spec.rb +80 -0
- data/spec/connection_spec.rb +37 -0
- data/spec/coverage_helper.rb +39 -0
- data/spec/dbus_spec.rb +22 -0
- data/spec/main_loop_spec.rb +14 -0
- data/spec/message_spec.rb +21 -0
- data/spec/mock-service/cockpit-dbustests.rb +29 -0
- data/spec/mock-service/com.redhat.Cockpit.DBusTests.xml +180 -0
- data/spec/mock-service/org.ruby.service.service +4 -0
- data/spec/mock-service/org.rubygems.ruby_dbus.DBusTests.service +4 -0
- data/spec/{service_newapi.rb → mock-service/spaghetti-monster.rb} +21 -10
- data/spec/node_spec.rb +1 -5
- data/spec/object_server_spec.rb +138 -0
- data/spec/object_spec.rb +46 -0
- data/spec/{bus_driver_spec.rb → proxy_service_spec.rb} +13 -8
- data/spec/spec_helper.rb +9 -45
- data/spec/thread_safety_spec.rb +9 -11
- data/spec/tools/dbus-launch-simple +4 -1
- data/spec/tools/dbus-limited-session.conf +3 -0
- data/spec/tools/test_env +26 -6
- metadata +24 -10
- data/spec/server_spec.rb +0 -55
- data/spec/service_spec.rb +0 -18
- 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
|
-
|
20
|
+
if @logger.nil?
|
21
|
+
debug = $DEBUG || ENV["RUBY_DBUS_DEBUG"]
|
21
22
|
@logger = Logger.new($stderr)
|
22
|
-
@logger.level =
|
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 ==
|
171
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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(
|
338
|
+
cur_intf.define(signal)
|
320
339
|
|
321
340
|
# ::Module#define_method(name) { body }
|
322
341
|
define_method(sym.to_s) do |*args|
|
data/lib/dbus/object_manager.rb
CHANGED
@@ -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
|
-
# {
|
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
|
-
|
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
|