ruby-dbus 0.22.1 → 0.23.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|