ruby-dbus 0.16.0 → 0.25.0
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 +288 -3
- data/README.md +3 -5
- data/Rakefile +27 -12
- data/VERSION +1 -1
- data/doc/Reference.md +109 -10
- data/examples/doc/_extract_examples +7 -0
- data/examples/no-bus/pulseaudio.rb +50 -0
- data/examples/no-introspect/nm-test.rb +2 -0
- data/examples/no-introspect/tracker-test.rb +3 -1
- data/examples/rhythmbox/playpause.rb +2 -1
- data/examples/service/call_service.rb +2 -1
- data/examples/service/complex-property.rb +21 -0
- data/examples/service/service_newapi.rb +3 -3
- data/examples/simple/call_introspect.rb +1 -0
- data/examples/simple/get_id.rb +6 -3
- data/examples/simple/properties.rb +2 -0
- data/examples/utils/listnames.rb +10 -1
- data/examples/utils/notify.rb +1 -0
- data/lib/dbus/api_options.rb +9 -0
- data/lib/dbus/auth.rb +307 -217
- data/lib/dbus/bus.rb +155 -588
- data/lib/dbus/bus_name.rb +12 -8
- data/lib/dbus/connection.rb +363 -0
- data/lib/dbus/core_ext/class/attribute.rb +1 -1
- data/lib/dbus/data.rb +821 -0
- data/lib/dbus/emits_changed_signal.rb +83 -0
- data/lib/dbus/error.rb +4 -2
- data/lib/dbus/introspect.rb +133 -31
- data/lib/dbus/logger.rb +6 -3
- data/lib/dbus/main.rb +66 -0
- data/lib/dbus/marshall.rb +247 -296
- data/lib/dbus/matchrule.rb +16 -16
- data/lib/dbus/message.rb +66 -57
- data/lib/dbus/message_queue.rb +40 -25
- data/lib/dbus/node_tree.rb +105 -0
- data/lib/dbus/object.rb +442 -33
- data/lib/dbus/object_manager.rb +61 -0
- data/lib/dbus/object_path.rb +11 -6
- data/lib/dbus/object_server.rb +149 -0
- data/lib/dbus/org.freedesktop.DBus.xml +97 -0
- data/lib/dbus/platform.rb +26 -0
- data/lib/dbus/proxy_object.rb +30 -11
- data/lib/dbus/proxy_object_factory.rb +13 -7
- data/lib/dbus/proxy_object_interface.rb +63 -30
- data/lib/dbus/proxy_service.rb +107 -0
- data/lib/dbus/raw_message.rb +91 -0
- data/lib/dbus/type.rb +319 -86
- data/lib/dbus/xml.rb +40 -19
- data/lib/dbus.rb +28 -17
- data/ruby-dbus.gemspec +13 -6
- data/spec/async_spec.rb +2 -0
- data/spec/auth_spec.rb +225 -0
- data/spec/binding_spec.rb +2 -0
- data/spec/bus_and_xml_backend_spec.rb +5 -1
- data/spec/bus_connection_spec.rb +93 -0
- data/spec/bus_name_spec.rb +3 -1
- data/spec/bus_spec.rb +2 -0
- data/spec/byte_array_spec.rb +2 -0
- data/spec/client_robustness_spec.rb +4 -2
- data/spec/connection_spec.rb +37 -0
- data/spec/coverage_helper.rb +39 -0
- data/spec/data/marshall.yaml +1667 -0
- data/spec/data_spec.rb +673 -0
- data/spec/dbus_spec.rb +22 -0
- data/spec/emits_changed_signal_spec.rb +58 -0
- data/spec/err_msg_spec.rb +2 -0
- data/spec/introspect_xml_parser_spec.rb +2 -0
- data/spec/introspection_spec.rb +2 -0
- data/spec/main_loop_spec.rb +17 -1
- 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} +122 -75
- data/spec/node_spec.rb +65 -0
- data/spec/object_manager_spec.rb +33 -0
- data/spec/object_path_spec.rb +3 -0
- data/spec/object_server_spec.rb +138 -0
- data/spec/object_spec.rb +194 -0
- data/spec/packet_marshaller_spec.rb +41 -0
- data/spec/packet_unmarshaller_spec.rb +248 -0
- data/spec/platform_spec.rb +14 -0
- data/spec/property_spec.rb +214 -12
- data/spec/proxy_object_interface_spec.rb +35 -0
- data/spec/proxy_object_spec.rb +2 -0
- data/spec/{bus_driver_spec.rb → proxy_service_spec.rb} +15 -8
- data/spec/raw_message_spec.rb +32 -0
- data/spec/server_robustness_spec.rb +20 -6
- data/spec/session_bus_spec.rb +36 -1
- data/spec/session_bus_spec_manual.rb +2 -0
- data/spec/signal_spec.rb +16 -3
- data/spec/spec_helper.rb +25 -33
- data/spec/thread_safety_spec.rb +55 -12
- data/spec/tools/dbus-launch-simple +9 -6
- data/spec/tools/dbus-limited-session.conf +32 -0
- data/spec/tools/test_env +26 -6
- data/spec/type_spec.rb +214 -6
- data/spec/value_spec.rb +16 -1
- data/spec/variant_spec.rb +4 -2
- data/spec/zzz_quit_spec.rb +16 -0
- metadata +92 -20
- data/examples/gdbus/gdbus +0 -257
- data/examples/gdbus/gdbus.glade +0 -98
- data/examples/gdbus/launch.sh +0 -4
- data/spec/server_spec.rb +0 -53
- data/spec/tools/test_server +0 -39
data/lib/dbus/object.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# This file is part of the ruby-dbus project
|
|
2
4
|
# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
|
|
3
5
|
#
|
|
@@ -6,46 +8,65 @@
|
|
|
6
8
|
# License, version 2.1 as published by the Free Software Foundation.
|
|
7
9
|
# See the file "COPYING" for the exact licensing terms.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
require "dbus/core_ext/class/attribute"
|
|
11
|
+
require_relative "core_ext/class/attribute"
|
|
11
12
|
|
|
12
13
|
module DBus
|
|
14
|
+
PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties"
|
|
15
|
+
|
|
13
16
|
# Exported object type
|
|
14
17
|
# = Exportable D-Bus object class
|
|
15
18
|
#
|
|
16
19
|
# Objects that are going to be exported by a D-Bus service
|
|
17
20
|
# should inherit from this class. At the client side, use {ProxyObject}.
|
|
18
21
|
class Object
|
|
19
|
-
# The path of the object.
|
|
22
|
+
# @return [ObjectPath] The path of the object.
|
|
20
23
|
attr_reader :path
|
|
24
|
+
|
|
21
25
|
# The interfaces that the object supports. Hash: String => Interface
|
|
22
26
|
my_class_attribute :intfs
|
|
23
27
|
self.intfs = {}
|
|
24
28
|
|
|
25
|
-
# The service that the object is exported by.
|
|
26
|
-
attr_writer :service
|
|
27
|
-
|
|
28
29
|
@@cur_intf = nil # Interface
|
|
29
30
|
@@intfs_mutex = Mutex.new
|
|
30
31
|
|
|
31
32
|
# Create a new object with a given _path_.
|
|
32
|
-
# Use
|
|
33
|
+
# Use ObjectServer#export to export it.
|
|
34
|
+
# @param path [ObjectPath] The path of the object.
|
|
33
35
|
def initialize(path)
|
|
34
36
|
@path = path
|
|
35
|
-
|
|
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
|
|
36
53
|
end
|
|
37
54
|
|
|
38
55
|
# Dispatch a message _msg_ to call exported methods
|
|
56
|
+
# @param msg [Message] only METHOD_CALLS do something
|
|
57
|
+
# @api private
|
|
39
58
|
def dispatch(msg)
|
|
40
59
|
case msg.message_type
|
|
41
60
|
when Message::METHOD_CALL
|
|
42
61
|
reply = nil
|
|
43
62
|
begin
|
|
44
|
-
|
|
63
|
+
iface = intfs[msg.interface]
|
|
64
|
+
if !iface
|
|
45
65
|
raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
|
|
46
66
|
"Interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
|
|
47
67
|
end
|
|
48
|
-
|
|
68
|
+
member_sym = msg.member.to_sym
|
|
69
|
+
meth = iface.methods[member_sym]
|
|
49
70
|
if !meth
|
|
50
71
|
raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
|
|
51
72
|
"Method \"#{msg.member}\" on interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
|
|
@@ -55,75 +76,463 @@ module DBus
|
|
|
55
76
|
retdata = [*retdata]
|
|
56
77
|
|
|
57
78
|
reply = Message.method_return(msg)
|
|
58
|
-
meth.rets.
|
|
59
|
-
|
|
79
|
+
rsigs = meth.rets.map(&:type)
|
|
80
|
+
rsigs.zip(retdata).each do |rsig, rdata|
|
|
81
|
+
reply.add_param(rsig, rdata)
|
|
60
82
|
end
|
|
61
|
-
rescue =>
|
|
62
|
-
dbus_msg_exc = msg.annotate_exception(
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
dbus_msg_exc = msg.annotate_exception(e)
|
|
63
85
|
reply = ErrorMessage.from_exception(dbus_msg_exc).reply_to(msg)
|
|
64
86
|
end
|
|
65
|
-
|
|
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)
|
|
66
90
|
end
|
|
67
91
|
end
|
|
68
92
|
|
|
69
93
|
# Select (and create) the interface that the following defined methods
|
|
70
94
|
# belong to.
|
|
71
|
-
|
|
95
|
+
# @param name [String] interface name like "org.example.ManagerManager"
|
|
96
|
+
# @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface
|
|
97
|
+
def self.dbus_interface(name)
|
|
72
98
|
@@intfs_mutex.synchronize do
|
|
73
|
-
@@cur_intf = intfs[
|
|
99
|
+
@@cur_intf = intfs[name]
|
|
74
100
|
if !@@cur_intf
|
|
75
|
-
@@cur_intf = Interface.new(
|
|
101
|
+
@@cur_intf = Interface.new(name) # validates the name
|
|
76
102
|
# As this is a mutable class_attr, we cannot use
|
|
77
|
-
# self.intfs[
|
|
103
|
+
# self.intfs[name] = @@cur_intf # Hash#[]=
|
|
78
104
|
# as that would modify parent class attr in place.
|
|
79
105
|
# Using the setter lets a subclass have the new value
|
|
80
106
|
# while the superclass keeps the old one.
|
|
81
|
-
self.intfs = intfs.merge(
|
|
107
|
+
self.intfs = intfs.merge(name => @@cur_intf)
|
|
108
|
+
end
|
|
109
|
+
begin
|
|
110
|
+
yield
|
|
111
|
+
ensure
|
|
112
|
+
@@cur_intf = nil
|
|
82
113
|
end
|
|
83
|
-
yield
|
|
84
|
-
@@cur_intf = nil
|
|
85
114
|
end
|
|
86
115
|
end
|
|
87
116
|
|
|
88
|
-
#
|
|
117
|
+
# Forgetting to declare the interface for a method/signal/property
|
|
118
|
+
# is a ScriptError.
|
|
89
119
|
class UndefinedInterface < ScriptError
|
|
90
120
|
def initialize(sym)
|
|
91
|
-
super "No interface specified for #{sym}"
|
|
121
|
+
super "No interface specified for #{sym}. Enclose it in dbus_interface."
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Declare the behavior of PropertiesChanged signal,
|
|
126
|
+
# common for all properties in this interface
|
|
127
|
+
# (individual properties may override it)
|
|
128
|
+
# @example
|
|
129
|
+
# self.emits_changed_signal = :invalidates
|
|
130
|
+
# @param [true,false,:const,:invalidates] value
|
|
131
|
+
def self.emits_changed_signal=(value)
|
|
132
|
+
raise UndefinedInterface, :emits_changed_signal if @@cur_intf.nil?
|
|
133
|
+
|
|
134
|
+
@@cur_intf.emits_changed_signal = EmitsChangedSignal.new(value)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# A read-write property accessing an instance variable.
|
|
138
|
+
# A combination of `attr_accessor` and {.dbus_accessor}.
|
|
139
|
+
#
|
|
140
|
+
# PropertiesChanged signal will be emitted whenever `foo_bar=` is used
|
|
141
|
+
# but not when @foo_bar is written directly.
|
|
142
|
+
#
|
|
143
|
+
# @param ruby_name [Symbol] :foo_bar is exposed as FooBar;
|
|
144
|
+
# use dbus_name to override
|
|
145
|
+
# @param type [Type,SingleCompleteType]
|
|
146
|
+
# a signature like "s" or "a(uus)" or Type::STRING
|
|
147
|
+
# @param dbus_name [String] if not given it is made
|
|
148
|
+
# by CamelCasing the ruby_name. foo_bar becomes FooBar
|
|
149
|
+
# to convert the Ruby convention to the DBus convention.
|
|
150
|
+
# @param emits_changed_signal [true,false,:const,:invalidates]
|
|
151
|
+
# see {EmitsChangedSignal}; if unspecified, ask the interface.
|
|
152
|
+
# @return [void]
|
|
153
|
+
def self.dbus_attr_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
|
|
154
|
+
attr_accessor(ruby_name)
|
|
155
|
+
|
|
156
|
+
dbus_accessor(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
|
|
157
|
+
end
|
|
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
|
+
|
|
170
|
+
# A read-only property accessing an instance variable.
|
|
171
|
+
# A combination of `attr_reader` and {.dbus_reader}.
|
|
172
|
+
#
|
|
173
|
+
# You may be instead looking for a variant which is read-write from the Ruby side:
|
|
174
|
+
# {.dbus_reader_attr_accessor}.
|
|
175
|
+
#
|
|
176
|
+
# Whenever the property value gets changed from "inside" the object,
|
|
177
|
+
# you should emit the `PropertiesChanged` signal by calling
|
|
178
|
+
# {#dbus_properties_changed}.
|
|
179
|
+
#
|
|
180
|
+
# dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
|
|
181
|
+
#
|
|
182
|
+
# or, omitting the value in the signal,
|
|
183
|
+
#
|
|
184
|
+
# dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
|
|
185
|
+
#
|
|
186
|
+
# @param (see .dbus_attr_accessor)
|
|
187
|
+
# @return (see .dbus_attr_accessor)
|
|
188
|
+
def self.dbus_attr_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
|
|
189
|
+
attr_reader(ruby_name)
|
|
190
|
+
|
|
191
|
+
dbus_reader(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# A write-only property accessing an instance variable.
|
|
195
|
+
# A combination of `attr_writer` and {.dbus_writer}.
|
|
196
|
+
#
|
|
197
|
+
# @param (see .dbus_attr_accessor)
|
|
198
|
+
# @return (see .dbus_attr_accessor)
|
|
199
|
+
def self.dbus_attr_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
|
|
200
|
+
attr_writer(ruby_name)
|
|
201
|
+
|
|
202
|
+
dbus_writer(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# A read-write property using a pair of reader/writer methods
|
|
206
|
+
# (which must already exist).
|
|
207
|
+
# (To directly access an instance variable, use {.dbus_attr_accessor} instead)
|
|
208
|
+
#
|
|
209
|
+
# Uses {.dbus_watcher} to set up the PropertiesChanged signal.
|
|
210
|
+
#
|
|
211
|
+
# @param (see .dbus_attr_accessor)
|
|
212
|
+
# @return (see .dbus_attr_accessor)
|
|
213
|
+
def self.dbus_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
|
|
214
|
+
raise UndefinedInterface, ruby_name if @@cur_intf.nil?
|
|
215
|
+
|
|
216
|
+
dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
|
|
217
|
+
property = Property.new(dbus_name, type, :readwrite, ruby_name: ruby_name)
|
|
218
|
+
@@cur_intf.define(property)
|
|
219
|
+
|
|
220
|
+
dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# A read-only property accessing a reader method (which must already exist).
|
|
224
|
+
# (To directly access an instance variable, use {.dbus_attr_reader} instead)
|
|
225
|
+
#
|
|
226
|
+
# At the D-Bus side the property is read only but it makes perfect sense to
|
|
227
|
+
# implement it with a read-write attr_accessor. In that case this method
|
|
228
|
+
# uses {.dbus_watcher} to set up the PropertiesChanged signal.
|
|
229
|
+
#
|
|
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"
|
|
236
|
+
#
|
|
237
|
+
# If the property value should change by other means than its attr_writer,
|
|
238
|
+
# you should emit the `PropertiesChanged` signal by calling
|
|
239
|
+
# {#dbus_properties_changed}.
|
|
240
|
+
#
|
|
241
|
+
# dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
|
|
242
|
+
#
|
|
243
|
+
# or, omitting the value in the signal,
|
|
244
|
+
#
|
|
245
|
+
# dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
|
|
246
|
+
#
|
|
247
|
+
# @param (see .dbus_attr_accessor)
|
|
248
|
+
# @return (see .dbus_attr_accessor)
|
|
249
|
+
def self.dbus_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
|
|
250
|
+
raise UndefinedInterface, ruby_name if @@cur_intf.nil?
|
|
251
|
+
|
|
252
|
+
dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
|
|
253
|
+
property = Property.new(dbus_name, type, :read, ruby_name: ruby_name)
|
|
254
|
+
@@cur_intf.define(property)
|
|
255
|
+
|
|
256
|
+
ruby_name_eq = :"#{ruby_name}="
|
|
257
|
+
return unless method_defined?(ruby_name_eq)
|
|
258
|
+
|
|
259
|
+
dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# A write-only property accessing a writer method (which must already exist).
|
|
263
|
+
# (To directly access an instance variable, use {.dbus_attr_writer} instead)
|
|
264
|
+
#
|
|
265
|
+
# Uses {.dbus_watcher} to set up the PropertiesChanged signal.
|
|
266
|
+
#
|
|
267
|
+
# @param (see .dbus_attr_accessor)
|
|
268
|
+
# @return (see .dbus_attr_accessor)
|
|
269
|
+
def self.dbus_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
|
|
270
|
+
raise UndefinedInterface, ruby_name if @@cur_intf.nil?
|
|
271
|
+
|
|
272
|
+
dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
|
|
273
|
+
property = Property.new(dbus_name, type, :write, ruby_name: ruby_name)
|
|
274
|
+
@@cur_intf.define(property)
|
|
275
|
+
|
|
276
|
+
dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Enables automatic sending of the PropertiesChanged signal.
|
|
280
|
+
# For *ruby_name* `foo_bar`, wrap `foo_bar=` so that it sends
|
|
281
|
+
# the signal for FooBar.
|
|
282
|
+
# The original version remains as `_original_foo_bar=`.
|
|
283
|
+
#
|
|
284
|
+
# @param ruby_name [Symbol] :foo_bar and :foo_bar= both mean the same thing
|
|
285
|
+
# @param dbus_name [String] if not given it is made
|
|
286
|
+
# by CamelCasing the ruby_name. foo_bar becomes FooBar
|
|
287
|
+
# to convert the Ruby convention to the DBus convention.
|
|
288
|
+
# @param emits_changed_signal [true,false,:const,:invalidates]
|
|
289
|
+
# see {EmitsChangedSignal}; if unspecified, ask the interface.
|
|
290
|
+
# @return [void]
|
|
291
|
+
def self.dbus_watcher(ruby_name, dbus_name: nil, emits_changed_signal: nil)
|
|
292
|
+
raise UndefinedInterface, ruby_name if @@cur_intf.nil?
|
|
293
|
+
|
|
294
|
+
interface_name = @@cur_intf.name
|
|
295
|
+
|
|
296
|
+
ruby_name = ruby_name.to_s.sub(/=$/, "").to_sym
|
|
297
|
+
ruby_name_eq = :"#{ruby_name}="
|
|
298
|
+
original_ruby_name_eq = "_original_#{ruby_name_eq}"
|
|
299
|
+
|
|
300
|
+
dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
|
|
301
|
+
|
|
302
|
+
emits_changed_signal = EmitsChangedSignal.new(emits_changed_signal, interface: @@cur_intf)
|
|
303
|
+
|
|
304
|
+
# the argument order is alias_method(new_name, existing_name)
|
|
305
|
+
alias_method original_ruby_name_eq, ruby_name_eq
|
|
306
|
+
define_method ruby_name_eq do |value|
|
|
307
|
+
result = public_send(original_ruby_name_eq, value)
|
|
308
|
+
|
|
309
|
+
case emits_changed_signal.value
|
|
310
|
+
when true
|
|
311
|
+
# signature: "interface:s, changed_props:a{sv}, invalidated_props:as"
|
|
312
|
+
dbus_properties_changed(interface_name, { dbus_name.to_s => value }, [])
|
|
313
|
+
when :invalidates
|
|
314
|
+
dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
|
|
315
|
+
when :const
|
|
316
|
+
# Oh my, seeing a value change of a supposedly constant property.
|
|
317
|
+
# Maybe should have raised at declaration time, don't make a fuss now.
|
|
318
|
+
when false
|
|
319
|
+
# Do nothing
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
result
|
|
92
323
|
end
|
|
93
324
|
end
|
|
94
325
|
|
|
95
326
|
# Defines an exportable method on the object with the given name _sym_,
|
|
96
327
|
# _prototype_ and the code in a block.
|
|
97
|
-
|
|
328
|
+
# @param prototype [Prototype]
|
|
329
|
+
def self.dbus_method(sym, prototype = "", &block)
|
|
98
330
|
raise UndefinedInterface, sym if @@cur_intf.nil?
|
|
99
|
-
|
|
100
|
-
|
|
331
|
+
|
|
332
|
+
@@cur_intf.define(Method.new(sym.to_s).from_prototype(prototype))
|
|
333
|
+
|
|
334
|
+
ruby_name = Object.make_method_name(@@cur_intf.name, sym.to_s)
|
|
335
|
+
# ::Module#define_method(name) { body }
|
|
336
|
+
define_method(ruby_name, &block)
|
|
101
337
|
end
|
|
102
338
|
|
|
103
339
|
# Emits a signal from the object with the given _interface_, signal
|
|
104
340
|
# _sig_ and arguments _args_.
|
|
341
|
+
# @param intf [Interface]
|
|
342
|
+
# @param sig [Signal]
|
|
343
|
+
# @param args arguments for the signal
|
|
105
344
|
def emit(intf, sig, *args)
|
|
106
|
-
|
|
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)
|
|
107
348
|
end
|
|
108
349
|
|
|
109
350
|
# Defines a signal for the object with a given name _sym_ and _prototype_.
|
|
110
|
-
def self.dbus_signal(sym,
|
|
351
|
+
def self.dbus_signal(sym, prototype = "")
|
|
111
352
|
raise UndefinedInterface, sym if @@cur_intf.nil?
|
|
353
|
+
|
|
112
354
|
cur_intf = @@cur_intf
|
|
113
|
-
signal = Signal.new(sym.to_s).from_prototype(
|
|
114
|
-
cur_intf.define(
|
|
355
|
+
signal = Signal.new(sym.to_s).from_prototype(prototype)
|
|
356
|
+
cur_intf.define(signal)
|
|
357
|
+
|
|
358
|
+
# ::Module#define_method(name) { body }
|
|
115
359
|
define_method(sym.to_s) do |*args|
|
|
116
360
|
emit(cur_intf, signal, *args)
|
|
117
361
|
end
|
|
118
362
|
end
|
|
119
363
|
|
|
120
|
-
####################################################################
|
|
121
|
-
|
|
122
364
|
# Helper method that returns a method name generated from the interface
|
|
123
365
|
# name _intfname_ and method name _methname_.
|
|
124
366
|
# @api private
|
|
125
367
|
def self.make_method_name(intfname, methname)
|
|
126
368
|
"#{intfname}%%#{methname}"
|
|
127
369
|
end
|
|
370
|
+
|
|
371
|
+
# TODO: borrow a proven implementation
|
|
372
|
+
# @param str [String]
|
|
373
|
+
# @return [String]
|
|
374
|
+
# @api private
|
|
375
|
+
def self.camelize(str)
|
|
376
|
+
str.split(/_/).map(&:capitalize).join("")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Make a D-Bus conventional name, CamelCased.
|
|
380
|
+
# @param ruby_name [String,Symbol] eg :do_something
|
|
381
|
+
# @param dbus_name [String,Symbol,nil] use this if given
|
|
382
|
+
# @return [Symbol] eg DoSomething
|
|
383
|
+
def self.make_dbus_name(ruby_name, dbus_name: nil)
|
|
384
|
+
dbus_name ||= camelize(ruby_name.to_s)
|
|
385
|
+
dbus_name.to_sym
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Use this instead of calling PropertiesChanged directly. This one
|
|
389
|
+
# considers not only the PC signature (which says that all property values
|
|
390
|
+
# are variants) but also the specific property type.
|
|
391
|
+
# @param interface_name [String] interface name like "org.example.ManagerManager"
|
|
392
|
+
# @param changed_props [Hash{String => ::Object}]
|
|
393
|
+
# changed properties (D-Bus names) and their values.
|
|
394
|
+
# @param invalidated_props [Array<String>]
|
|
395
|
+
# names of properties whose changed value is not specified
|
|
396
|
+
def dbus_properties_changed(interface_name, changed_props, invalidated_props)
|
|
397
|
+
typed_changed_props = changed_props.map do |dbus_name, value|
|
|
398
|
+
property = dbus_lookup_property(interface_name, dbus_name)
|
|
399
|
+
type = property.type
|
|
400
|
+
typed_value = Data.make_typed(type, value)
|
|
401
|
+
variant = Data::Variant.new(typed_value, member_type: type)
|
|
402
|
+
[dbus_name, variant]
|
|
403
|
+
end.to_h
|
|
404
|
+
PropertiesChanged(interface_name, typed_changed_props, invalidated_props)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# @param interface_name [String]
|
|
408
|
+
# @param property_name [String]
|
|
409
|
+
# @return [Property]
|
|
410
|
+
# @raise [DBus::Error]
|
|
411
|
+
# @api private
|
|
412
|
+
def dbus_lookup_property(interface_name, property_name)
|
|
413
|
+
# what should happen for unknown properties
|
|
414
|
+
# plasma: InvalidArgs (propname), UnknownInterface (interface)
|
|
415
|
+
# systemd: UnknownProperty
|
|
416
|
+
interface = intfs[interface_name]
|
|
417
|
+
if !interface
|
|
418
|
+
raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
|
|
419
|
+
"Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found: no such interface"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
property = interface.properties[property_name.to_sym]
|
|
423
|
+
if !property
|
|
424
|
+
raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
|
|
425
|
+
"Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
property
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Generates information about interfaces and properties of the object
|
|
432
|
+
#
|
|
433
|
+
# Returns a hash containing interfaces names as keys. Each value is the
|
|
434
|
+
# same hash that would be returned by the
|
|
435
|
+
# org.freedesktop.DBus.Properties.GetAll() method for that combination of
|
|
436
|
+
# object path and interface. If an interface has no properties, the empty
|
|
437
|
+
# hash is returned.
|
|
438
|
+
#
|
|
439
|
+
# @return [Hash{String => Hash{String => Data::Base}}] interface -> property -> value
|
|
440
|
+
def interfaces_and_properties
|
|
441
|
+
get_all_method = self.class.make_method_name("org.freedesktop.DBus.Properties", :GetAll)
|
|
442
|
+
|
|
443
|
+
intfs.keys.each_with_object({}) do |interface, hash|
|
|
444
|
+
hash[interface] = public_send(get_all_method, interface).first
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
####################################################################
|
|
449
|
+
|
|
450
|
+
# use the above defined methods to declare the property-handling
|
|
451
|
+
# interfaces and methods
|
|
452
|
+
|
|
453
|
+
dbus_interface PROPERTY_INTERFACE do
|
|
454
|
+
dbus_method :Get, "in interface_name:s, in property_name:s, out value:v" do |interface_name, property_name|
|
|
455
|
+
property = dbus_lookup_property(interface_name, property_name)
|
|
456
|
+
|
|
457
|
+
if property.readable?
|
|
458
|
+
begin
|
|
459
|
+
ruby_name = property.ruby_name
|
|
460
|
+
value = public_send(ruby_name)
|
|
461
|
+
# may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
|
|
462
|
+
typed_value = Data.make_typed(property.type, value)
|
|
463
|
+
[typed_value]
|
|
464
|
+
rescue StandardError => e
|
|
465
|
+
msg = "When getting '#{interface_name}.#{property_name}': " + e.message
|
|
466
|
+
raise e.exception(msg)
|
|
467
|
+
end
|
|
468
|
+
else
|
|
469
|
+
raise DBus.error("org.freedesktop.DBus.Error.PropertyWriteOnly"),
|
|
470
|
+
"Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not readable"
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
dbus_method :Set, "in interface_name:s, in property_name:s, in val:v" do |interface_name, property_name, value|
|
|
475
|
+
property = dbus_lookup_property(interface_name, property_name)
|
|
476
|
+
|
|
477
|
+
if property.writable?
|
|
478
|
+
begin
|
|
479
|
+
ruby_name_eq = "#{property.ruby_name}="
|
|
480
|
+
# TODO: declare dbus_method :Set to take :exact argument
|
|
481
|
+
# and type check it here before passing its :plain value
|
|
482
|
+
# to the implementation
|
|
483
|
+
public_send(ruby_name_eq, value)
|
|
484
|
+
rescue StandardError => e
|
|
485
|
+
msg = "When setting '#{interface_name}.#{property_name}': " + e.message
|
|
486
|
+
raise e.exception(msg)
|
|
487
|
+
end
|
|
488
|
+
else
|
|
489
|
+
raise DBus.error("org.freedesktop.DBus.Error.PropertyReadOnly"),
|
|
490
|
+
"Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not writable"
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
dbus_method :GetAll, "in interface_name:s, out value:a{sv}" do |interface_name|
|
|
495
|
+
interface = intfs[interface_name]
|
|
496
|
+
if !interface
|
|
497
|
+
raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
|
|
498
|
+
"Properties '#{interface_name}.*' (on object '#{@path}') not found: no such interface"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
p_hash = {}
|
|
502
|
+
interface.properties.each do |p_name, property|
|
|
503
|
+
next unless property.readable?
|
|
504
|
+
|
|
505
|
+
ruby_name = property.ruby_name
|
|
506
|
+
begin
|
|
507
|
+
# D-Bus spec says:
|
|
508
|
+
# > If GetAll is called with a valid interface name for which some
|
|
509
|
+
# > properties are not accessible to the caller (for example, due
|
|
510
|
+
# > to per-property access control implemented in the service),
|
|
511
|
+
# > those properties should be silently omitted from the result
|
|
512
|
+
# > array.
|
|
513
|
+
# so we will silently omit properties that fail to read.
|
|
514
|
+
# Get'ting them individually will send DBus.Error
|
|
515
|
+
value = public_send(ruby_name)
|
|
516
|
+
# may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
|
|
517
|
+
typed_value = Data.make_typed(property.type, value)
|
|
518
|
+
p_hash[p_name.to_s] = typed_value
|
|
519
|
+
rescue StandardError
|
|
520
|
+
DBus.logger.debug "Property '#{interface_name}.#{p_name}' (on object '#{@path}') " \
|
|
521
|
+
"has raised during GetAll, omitting it"
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
[p_hash]
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
dbus_signal :PropertiesChanged, "interface:s, changed_properties:a{sv}, invalidated_properties:as"
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
dbus_interface "org.freedesktop.DBus.Introspectable" do
|
|
532
|
+
dbus_method :Introspect, "out xml_data:s" do
|
|
533
|
+
# The body is not used, Connection#process handles it instead
|
|
534
|
+
# which is more efficient and handles paths without objects.
|
|
535
|
+
end
|
|
536
|
+
end
|
|
128
537
|
end
|
|
129
538
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is part of the ruby-dbus project
|
|
4
|
+
# Copyright (C) 2022 José Iván López González
|
|
5
|
+
# Copyright (C) 2022 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
|
+
# A mixin for {DBus::Object} implementing
|
|
14
|
+
# {https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
|
|
15
|
+
# org.freedesktop.DBus.ObjectManager}.
|
|
16
|
+
#
|
|
17
|
+
# {ObjectServer#export} and {ObjectServer#unexport} will look for an ObjectManager
|
|
18
|
+
# parent in the path hierarchy. If found, it will emit InterfacesAdded
|
|
19
|
+
# or InterfacesRemoved, as appropriate.
|
|
20
|
+
module ObjectManager
|
|
21
|
+
OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"
|
|
22
|
+
|
|
23
|
+
# Implements `the GetManagedObjects` method.
|
|
24
|
+
# @return [Hash{ObjectPath => Hash{String => Hash{String => Data::Base}}}]
|
|
25
|
+
# object -> interface -> property -> value
|
|
26
|
+
def managed_objects
|
|
27
|
+
descendant_objects = object_server.descendants_for(path)
|
|
28
|
+
descendant_objects.each_with_object({}) do |obj, hash|
|
|
29
|
+
hash[obj.path] = obj.interfaces_and_properties
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# {ObjectServer#export} will call this for you to emit the `InterfacesAdded` signal.
|
|
34
|
+
# @param object [DBus::Object]
|
|
35
|
+
# @return [void]
|
|
36
|
+
def object_added(object)
|
|
37
|
+
InterfacesAdded(object.path, object.interfaces_and_properties)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# {ObjectServer#unexport} will call this for you to emit the `InterfacesRemoved` signal.
|
|
41
|
+
# @param object [DBus::Object]
|
|
42
|
+
# @return [void]
|
|
43
|
+
def object_removed(object)
|
|
44
|
+
InterfacesRemoved(object.path, object.intfs.keys)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Module#included, a hook for `include ObjectManager`, declares its dbus_interface.
|
|
48
|
+
def self.included(base)
|
|
49
|
+
base.class_eval do
|
|
50
|
+
dbus_interface OBJECT_MANAGER_INTERFACE do
|
|
51
|
+
dbus_method :GetManagedObjects, "out res:a{oa{sa{sv}}}" do
|
|
52
|
+
[managed_objects]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
dbus_signal :InterfacesAdded, "object:o, interfaces_and_properties:a{sa{sv}}"
|
|
56
|
+
dbus_signal :InterfacesRemoved, "object:o, interfaces:as"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/dbus/object_path.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# This file is part of the ruby-dbus project
|
|
2
4
|
# Copyright (C) 2019 Martin Vidner
|
|
3
5
|
#
|
|
@@ -7,18 +9,21 @@
|
|
|
7
9
|
# See the file "COPYING" for the exact licensing terms.
|
|
8
10
|
|
|
9
11
|
module DBus
|
|
10
|
-
# A {::String} that validates at initialization time
|
|
12
|
+
# A {::String} that validates at initialization time.
|
|
13
|
+
# See also {DBus::Data::ObjectPath}
|
|
14
|
+
# @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
|
|
11
15
|
class ObjectPath < String
|
|
12
16
|
# @raise Error if not a valid object path
|
|
13
|
-
def initialize(
|
|
14
|
-
unless self.class.valid?(
|
|
15
|
-
raise DBus::Error, "Invalid object path #{
|
|
17
|
+
def initialize(str)
|
|
18
|
+
unless self.class.valid?(str)
|
|
19
|
+
raise DBus::Error, "Invalid object path #{str.inspect}"
|
|
16
20
|
end
|
|
21
|
+
|
|
17
22
|
super
|
|
18
23
|
end
|
|
19
24
|
|
|
20
|
-
def self.valid?(
|
|
21
|
-
|
|
25
|
+
def self.valid?(str)
|
|
26
|
+
str == "/" || str =~ %r{\A(/[A-Za-z0-9_]+)+\z}
|
|
22
27
|
end
|
|
23
28
|
end
|
|
24
29
|
end
|