Skip to content

Commit

Permalink
Merge c482939 into 44dac02
Browse files Browse the repository at this point in the history
  • Loading branch information
mvidner committed Jun 17, 2022
2 parents 44dac02 + c482939 commit e35057d
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 39 deletions.
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Bug fixes:
guess the types.

API:
* Object.dbus_watcher now needs an additional _type_ argument.
* Service side `emits_changed_signal`: can be assigned within
`dbus_interface` or as an option when declaring properties.

[#115]: https://github.com/mvidner/ruby-dbus/issues/115

Expand Down
1 change: 1 addition & 0 deletions lib/dbus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require_relative "dbus/bus"
require_relative "dbus/bus_name"
require_relative "dbus/data"
require_relative "dbus/emits_changed_signal"
require_relative "dbus/error"
require_relative "dbus/introspect"
require_relative "dbus/logger"
Expand Down
10 changes: 3 additions & 7 deletions lib/dbus/bus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,11 @@ def to_xml(node_opath)
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
'
xml += "<node name=\"#{node_opath}\">\n"
each_pair do |k, _v|
each_key do |k|
xml += " <node name=\"#{k}\" />\n"
end
@object&.intfs&.each_pair do |_k, v|
xml += " <interface name=\"#{v.name}\">\n"
v.methods.each_value { |m| xml += m.to_xml }
v.signals.each_value { |m| xml += m.to_xml }
v.properties.each_value { |m| xml += m.to_xml }
xml += " </interface>\n"
@object&.intfs&.each_value do |v|
xml += v.to_xml
end
xml += "</node>"
xml
Expand Down
77 changes: 77 additions & 0 deletions lib/dbus/emits_changed_signal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

# This file is part of the ruby-dbus project
# Copyright (C) 2022 Martin Vidner
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License, version 2.1 as published by the Free Software Foundation.
# See the file "COPYING" for the exact licensing terms.

module DBus
# Describes the behavior of PropertiesChanged signal, for a single property
# or for an entire interface.
#
# The possible values are:
#
# - *true*: the signal is emitted with the value included.
# - *:invalidates*: the signal is emitted but the value is not included
# in the signal.
# - *:const*: the property never changes value during the lifetime
# of the object it belongs to, and hence the signal
# is never emitted for it (but clients can cache the value)
# - *false*: the signal won't be emitted (clients should re-Get the property value)
#
# The default is:
# - for an interface: *true*
# - for a property: what the parent interface specifies
#
# See also
# https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format
#
# Immutable once constructed.
class EmitsChangedSignal
# @return [true,false,:const,:invalidates]
attr_reader :value

# @param value [true,false,:const,:invalidates,nil]
# See class-level description above, {EmitsChangedSignal}.
# @param interface [Interface,nil]
# If the (property-level) *value* is unspecified (nil), this is the
# containing {Interface} to get the value from.
def initialize(value, interface: nil)
if value.nil?
raise ArgumentError, "Both arguments are nil" if interface.nil?

@value = interface.emits_changed_signal.value
else
expecting = [true, false, :const, :invalidates]
unless expecting.include?(value)
raise ArgumentError, "Expecting one of #{expecting.inspect}. Seen #{value.inspect}"
end

@value = value
end

freeze
end

# Return introspection XML string representation
# @return [String]
def to_xml
return "" if @value == true

" <annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"#{@value}\"/>\n"
end

def to_s
@value.to_s
end

def inspect
@value.inspect
end

DEFAULT_ECS = EmitsChangedSignal.new(true)
end
end
32 changes: 31 additions & 1 deletion lib/dbus/introspect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class InvalidClassDefinition < Exception
# method call instantiates and configures this class for us.
#
# It also is the local definition of interface exported by the program.
# At the client side, see ProxyObjectInterface
# At the client side, see {ProxyObjectInterface}.
class Interface
# @return [String] The name of the interface.
attr_reader :name
Expand All @@ -38,13 +38,30 @@ class Interface
# @return [Hash{Symbol => Property}]
attr_reader :properties

# @return [EmitsChangedSignal]
attr_reader :emits_changed_signal

# Creates a new interface with a given _name_.
def initialize(name)
validate_name(name)
@name = name
@methods = {}
@signals = {}
@properties = {}
@emits_changed_signal = EmitsChangedSignal::DEFAULT_ECS
end

# Helper for {Object.emits_changed_signal=}.
# @api private
def emits_changed_signal=(ecs)
raise TypeError unless ecs.is_a? EmitsChangedSignal
# equal?: object identity
unless @emits_changed_signal.equal?(EmitsChangedSignal::DEFAULT_ECS) ||
@emits_changed_signal.value == ecs.value
raise "emits_change_signal was assigned more than once"
end

@emits_changed_signal = ecs
end

# Validates a service _name_.
Expand Down Expand Up @@ -85,6 +102,18 @@ def define_method(id, prototype)
define(m)
end
alias declare_method define_method

# Return introspection XML string representation of the property.
# @return [String]
def to_xml
xml = " <interface name=\"#{name}\">\n"
xml += emits_changed_signal.to_xml
methods.each_value { |m| xml += m.to_xml }
signals.each_value { |m| xml += m.to_xml }
properties.each_value { |m| xml += m.to_xml }
xml += " </interface>\n"
xml
end
end

# = A formal parameter has a name and a type
Expand Down Expand Up @@ -190,6 +219,7 @@ def from_prototype(prototype)
end

# Return an XML string representation of the method interface elment.
# @return [String]
def to_xml
xml = " <method name=\"#{@name}\">\n"
@params.each do |param|
Expand Down
89 changes: 59 additions & 30 deletions lib/dbus/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ def initialize(sym)
end
end

# Declare the behavior of PropertiesChanged signal,
# common for all properties in this interface
# (individual properties may override it)
# @param [true,false,:const,:invalidates] value
def self.emits_changed_signal=(value)
raise UndefinedInterface, :emits_changed_signal if @@cur_intf.nil?

# FIXME: if the property-level ECS refers to interface-level ECS
# at DECLARATION time then we need to make sure that this is called
# before the individual properties.
# that is prop should check that this is non-nil?

# make it simple: assume we can declare this any time
@@cur_intf.emits_changed_signal = EmitsChangedSignal.new(value)
end

# A read-write property accessing an instance variable.
# A combination of `attr_accessor` and {.dbus_accessor}.
#
Expand All @@ -114,11 +130,13 @@ def initialize(sym)
# @param dbus_name [String] if not given it is made
# by CamelCasing the ruby_name. foo_bar becomes FooBar
# to convert the Ruby convention to the DBus convention.
# @param emits_changed_signal [true,false,:const,:invalidates]
# see {EmitsChangedSignal}; if unspecified, ask the interface.
# @return [void]
def self.dbus_attr_accessor(ruby_name, type, dbus_name: nil)
def self.dbus_attr_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
attr_accessor(ruby_name)

dbus_accessor(ruby_name, type, dbus_name: dbus_name)
dbus_accessor(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
end

# A read-only property accessing an instance variable.
Expand All @@ -136,21 +154,21 @@ def self.dbus_attr_accessor(ruby_name, type, dbus_name: nil)
#
# @param (see .dbus_attr_accessor)
# @return (see .dbus_attr_accessor)
def self.dbus_attr_reader(ruby_name, type, dbus_name: nil)
def self.dbus_attr_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
attr_reader(ruby_name)

dbus_reader(ruby_name, type, dbus_name: dbus_name)
dbus_reader(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
end

# A write-only property accessing an instance variable.
# A combination of `attr_writer` and {.dbus_writer}.
#
# @param (see .dbus_attr_accessor)
# @return (see .dbus_attr_accessor)
def self.dbus_attr_writer(ruby_name, type, dbus_name: nil)
def self.dbus_attr_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
attr_writer(ruby_name)

dbus_writer(ruby_name, type, dbus_name: dbus_name)
dbus_writer(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
end

# A read-write property using a pair of reader/writer methods
Expand All @@ -161,20 +179,27 @@ def self.dbus_attr_writer(ruby_name, type, dbus_name: nil)
#
# @param (see .dbus_attr_accessor)
# @return (see .dbus_attr_accessor)
def self.dbus_accessor(ruby_name, type, dbus_name: nil)
def self.dbus_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
raise UndefinedInterface, ruby_name if @@cur_intf.nil?

dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
property = Property.new(dbus_name, type, :readwrite, ruby_name: ruby_name)
@@cur_intf.define(property)

dbus_watcher(ruby_name, dbus_name: dbus_name, type: property.type)
dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
end

# A read-only property accessing a reader method (which must already exist).
# (To directly access an instance variable, use {.dbus_attr_reader} instead)
#
# Whenever the property value gets changed from "inside" the object,
# At the D-Bus side the property is read only but it makes perfect sense to
# implement it with a read-write attr_accessor. In that case this method
# uses {.dbus_watcher} to set up the PropertiesChanged signal.
#
# attr_accessor :foo_bar
# dbus_reader :foo_bar, "s"
#
# If the property value should change by other means than its attr_writer,
# you should emit the `PropertiesChanged` signal by calling
# {#dbus_properties_changed}.
#
Expand All @@ -186,12 +211,17 @@ def self.dbus_accessor(ruby_name, type, dbus_name: nil)
#
# @param (see .dbus_attr_accessor)
# @return (see .dbus_attr_accessor)
def self.dbus_reader(ruby_name, type, dbus_name: nil)
def self.dbus_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
raise UndefinedInterface, ruby_name if @@cur_intf.nil?

dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
property = Property.new(dbus_name, type, :read, ruby_name: ruby_name)
@@cur_intf.define(property)

ruby_name_eq = "#{ruby_name}=".to_sym
return unless method_defined?(ruby_name_eq)

dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
end

# A write-only property accessing a writer method (which must already exist).
Expand All @@ -201,14 +231,14 @@ def self.dbus_reader(ruby_name, type, dbus_name: nil)
#
# @param (see .dbus_attr_accessor)
# @return (see .dbus_attr_accessor)
def self.dbus_writer(ruby_name, type, dbus_name: nil)
def self.dbus_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
raise UndefinedInterface, ruby_name if @@cur_intf.nil?

dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
property = Property.new(dbus_name, type, :write, ruby_name: ruby_name)
@@cur_intf.define(property)

dbus_watcher(ruby_name, dbus_name: dbus_name, type: property.type)
dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
end

# Enables automatic sending of the PropertiesChanged signal.
Expand All @@ -220,9 +250,10 @@ def self.dbus_writer(ruby_name, type, dbus_name: nil)
# @param dbus_name [String] if not given it is made
# by CamelCasing the ruby_name. foo_bar becomes FooBar
# to convert the Ruby convention to the DBus convention.
# @param type [SingleCompleteType,Type] the type of the property
# @param emits_changed_signal [true,false,:const,:invalidates]
# see {EmitsChangedSignal}; if unspecified, ask the interface.
# @return [void]
def self.dbus_watcher(ruby_name, type:, dbus_name: nil)
def self.dbus_watcher(ruby_name, dbus_name: nil, emits_changed_signal: nil)
raise UndefinedInterface, ruby_name if @@cur_intf.nil?

interface_name = @@cur_intf.name
Expand All @@ -233,17 +264,25 @@ def self.dbus_watcher(ruby_name, type:, dbus_name: nil)

dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)

emits_changed_signal = EmitsChangedSignal.new(emits_changed_signal, interface: @@cur_intf)

# the argument order is alias_method(new_name, existing_name)
alias_method original_ruby_name_eq, ruby_name_eq
define_method ruby_name_eq do |value|
result = public_send(original_ruby_name_eq, value)

dbus_property_changed4(
interface_name: interface_name,
dbus_name: dbus_name,
value: value,
type: type
)
case emits_changed_signal.value
when true
# signature: "interface:s, changed_props:a{sv}, invalidated_props:as"
dbus_properties_changed(interface_name, { dbus_name.to_s => value }, [])
when :invalidates
dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
when :const
# Oh my, seeing a value change of a supposedly constant property.
# Maybe should have raised at declaration time, don't make a fuss now.
when false
# Do nothing
end

result
end
Expand Down Expand Up @@ -328,16 +367,6 @@ def dbus_properties_changed(interface_name, changed_props, invalidated_props)
PropertiesChanged(interface_name, typed_changed_props, invalidated_props)
end

# 4 individual necessary arguments
# @api private
def dbus_property_changed4(interface_name:, dbus_name:, value:, type:)
# TODO: respect EmitsChangedSignal to use invalidated_properties instead

typed_value = Data.make_typed(type, value)
variant = Data::Variant.new(typed_value, member_type: type)
PropertiesChanged(interface_name, { dbus_name.to_s => variant }, [])
end

# @param interface_name [String]
# @param property_name [String]
# @return [Property]
Expand Down

0 comments on commit e35057d

Please sign in to comment.