dnssd 1.1.0 → 1.2
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.
- data.tar.gz.sig +0 -0
- data/.autotest +10 -11
- data/History.txt +28 -3
- data/Manifest.txt +4 -2
- data/README.txt +10 -8
- data/ext/dnssd/dnssd.c +81 -4
- data/ext/dnssd/dnssd.h +3 -1
- data/ext/dnssd/errors.c +61 -39
- data/ext/dnssd/extconf.rb +4 -1
- data/ext/dnssd/flags.c +84 -22
- data/ext/dnssd/service.c +242 -553
- data/lib/dnssd.rb +138 -109
- data/lib/dnssd/flags.rb +13 -1
- data/lib/dnssd/reply.rb +71 -2
- data/lib/dnssd/service.rb +147 -4
- data/sample/enumerate_domains.rb +13 -0
- data/sample/resolve.rb +2 -2
- data/sample/server.rb +19 -0
- data/sample/socket.rb +18 -0
- data/test/test_dnssd.rb +75 -0
- data/test/test_dnssd_flags.rb +6 -1
- data/test/test_dnssd_reply.rb +67 -0
- metadata +30 -6
- metadata.gz.sig +0 -0
- data/ext/dnssd/dns_sd.h +0 -1493
- data/sample/highlevel_api.rb +0 -30
data/lib/dnssd.rb
CHANGED
@@ -1,153 +1,182 @@
|
|
1
1
|
require 'dnssd/dnssd'
|
2
|
+
require 'socket'
|
2
3
|
|
3
4
|
##
|
4
|
-
# DNSSD is a wrapper for
|
5
|
-
#
|
6
|
-
# DNSSD.
|
7
|
-
#
|
5
|
+
# DNSSD is a wrapper for the DNS Service Discovery library.
|
6
|
+
#
|
7
|
+
# DNSSD.announce and DNSSD::Reply.connect provide an easy-to-use way to
|
8
|
+
# announce and connect to services.
|
9
|
+
#
|
10
|
+
# The methods DNSSD.enumerate_domains, DNSSD.browse, DNSSD.register, and
|
11
|
+
# DNSSD.resolve provide the basic API for making your applications DNS \Service
|
12
|
+
# Discovery aware.
|
8
13
|
|
9
14
|
module DNSSD
|
10
|
-
VERSION = '1.1.0'
|
11
|
-
end
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
require 'dnssd/service'
|
16
|
-
require 'dnssd/text_record'
|
16
|
+
##
|
17
|
+
# The version of DNSSD you're using.
|
17
18
|
|
18
|
-
=
|
19
|
+
VERSION = '1.2'
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
##
|
22
|
+
# Registers +socket+ with DNSSD as +name+. If +service+ is omitted it is
|
23
|
+
# looked up using #getservbyport and the ports address. +text_record+,
|
24
|
+
# +flags+ and +interface+ are used as in #register.
|
25
|
+
#
|
26
|
+
# Returns the Service created by registering the socket. The Service will
|
27
|
+
# automatically be shut down when #close or #close_read is called on the
|
28
|
+
# socket.
|
29
|
+
#
|
30
|
+
# Only for bound TCP and UDP sockets.
|
23
31
|
|
24
|
-
def self.
|
25
|
-
|
26
|
-
|
32
|
+
def self.announce(socket, name, service = nil, text_record = nil, flags = 0,
|
33
|
+
interface = DNSSD::InterfaceAny, &block)
|
34
|
+
_, port, _, address = socket.addr
|
27
35
|
|
28
|
-
|
36
|
+
raise ArgumentError, 'socket not bound' if port == 0
|
29
37
|
|
30
|
-
|
31
|
-
attr_accessor :name, :port, :iface
|
32
|
-
def initialize(port, name=nil, iface=0)
|
33
|
-
@name = name
|
34
|
-
@port = port
|
35
|
-
@iface = iface
|
36
|
-
end
|
37
|
-
end
|
38
|
+
service ||= DNSSD.getservbyport port
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
40
|
+
proto = case socket
|
41
|
+
when TCPSocket then 'tcp'
|
42
|
+
when UDPSocket then 'udp'
|
43
|
+
else raise ArgumentError, 'tcp or udp sockets only'
|
44
|
+
end
|
45
45
|
|
46
|
-
|
46
|
+
type = "_#{service}._#{proto}"
|
47
47
|
|
48
|
-
|
48
|
+
registrar = register(name, type, nil, port, text_record, flags, interface,
|
49
|
+
&block)
|
50
|
+
|
51
|
+
socket.instance_variable_set :@registrar, registrar
|
52
|
+
|
53
|
+
def socket.close
|
54
|
+
result = super
|
49
55
|
@registrar.stop
|
56
|
+
return result
|
50
57
|
end
|
51
58
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
location
|
59
|
+
def socket.close_read
|
60
|
+
result = super
|
61
|
+
@registrar.stop
|
62
|
+
return result
|
57
63
|
end
|
58
64
|
|
59
|
-
|
60
|
-
|
61
|
-
raise MalformedDomain.new("#{domain} is not a valid domain name")
|
62
|
-
end
|
63
|
-
domain
|
64
|
-
end
|
65
|
+
registrar
|
66
|
+
end
|
65
67
|
|
66
|
-
|
67
|
-
|
68
|
-
@registrar = register(@name, @type, @domain, @location.port, TextRecord.new) do |service, name, type, domain|
|
69
|
-
@name = name
|
70
|
-
@type = type
|
71
|
-
@domain = domain
|
72
|
-
thread.wakeup
|
73
|
-
end
|
74
|
-
Thread.stop
|
75
|
-
end
|
68
|
+
##
|
69
|
+
# Asynchronous version of DNSSD::Service#browse
|
76
70
|
|
77
|
-
|
78
|
-
|
71
|
+
def self.browse(type, domain = nil, flags = 0,
|
72
|
+
interface = DNSSD::InterfaceAny, &block)
|
73
|
+
service = DNSSD::Service.new
|
74
|
+
|
75
|
+
Thread.start do
|
76
|
+
run(service, :browse, type, domain, flags, interface, &block)
|
79
77
|
end
|
80
78
|
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
service
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Synchronous version of DNSSD::Service#browse
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
85
|
+
def self.browse!(type, domain = nil, flags = 0,
|
86
|
+
interface = DNSSD::InterfaceAny, &block)
|
87
|
+
service = DNSSD::Service.new
|
88
|
+
|
89
|
+
run(service, :browse, type, domain, flags, interface, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Asynchronous version of DNSSD::Service#enumerate_domains
|
94
|
+
|
95
|
+
def self.enumerate_domains(flags = DNSSD::Flags::BrowseDomains,
|
96
|
+
interface = DNSSD::InterfaceAny, &block)
|
97
|
+
service = DNSSD::Service.new
|
98
|
+
|
99
|
+
Thread.start do
|
100
|
+
run(service, :enumerate_domains, flags, interface, &block)
|
90
101
|
end
|
102
|
+
|
103
|
+
service
|
91
104
|
end
|
92
105
|
|
93
|
-
|
106
|
+
##
|
107
|
+
# Synchronous version of DNSSD::Service#enumerate_domains
|
94
108
|
|
95
|
-
|
109
|
+
def self.enumerate_domains!(flags = DNSSD::Flags::BrowseDomains,
|
110
|
+
interface = DNSSD::InterfaceAny, &block)
|
111
|
+
service = DNSSD::Service.new
|
96
112
|
|
97
|
-
|
98
|
-
|
99
|
-
self.to_s == other.to_s
|
100
|
-
end
|
113
|
+
run(service, :enumerate_domains, flags, interface, &block)
|
114
|
+
end
|
101
115
|
|
102
|
-
|
103
|
-
|
104
|
-
end
|
116
|
+
##
|
117
|
+
# Asynchronous version of DNSSD::Service#register
|
105
118
|
|
106
|
-
|
107
|
-
|
108
|
-
|
119
|
+
def self.register(name, type, domain, port, text_record = nil, flags = 0,
|
120
|
+
interface = DNSSD::InterfaceAny, &block)
|
121
|
+
service = DNSSD::Service.new
|
109
122
|
|
110
|
-
|
111
|
-
|
112
|
-
|
123
|
+
Thread.start do
|
124
|
+
run(service, :register, name, type, domain, port, nil, text_record,
|
125
|
+
flags, interface, &block)
|
113
126
|
end
|
114
127
|
|
115
|
-
|
116
|
-
|
117
|
-
@change_listeners << block
|
118
|
-
end
|
128
|
+
service
|
129
|
+
end
|
119
130
|
|
120
|
-
|
121
|
-
|
122
|
-
@add_listeners << block
|
123
|
-
end
|
131
|
+
##
|
132
|
+
# Synchronous version of DNSSD::Service#register
|
124
133
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
end
|
134
|
+
def self.register!(name, type, domain, port, text_record = nil, flags = 0,
|
135
|
+
interface = DNSSD::InterfaceAny, &block)
|
136
|
+
service = DNSSD::Service.new
|
129
137
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|service, name, type, domain, operation, interface|
|
134
|
-
context = Context.new(service, name, type, domain, operation, interface)
|
135
|
-
puts "Name: #{name} Type: #{type} Domain: #{domain} Operation: #{operation} Interface: #{interface}"
|
136
|
-
end
|
137
|
-
end
|
138
|
+
run(service, :register, name, type, domain, port, nil, text_record, flags,
|
139
|
+
interface, &block)
|
140
|
+
end
|
138
141
|
|
139
|
-
|
140
|
-
|
141
|
-
end
|
142
|
+
##
|
143
|
+
# Asynchronous version of DNSSD::Service#resolve
|
142
144
|
|
143
|
-
|
144
|
-
|
145
|
-
end
|
145
|
+
def self.resolve(*args, &block)
|
146
|
+
service = DNSSD::Service.new
|
146
147
|
|
147
|
-
|
148
|
-
|
148
|
+
Thread.start do
|
149
|
+
run(service, :resolve, *args, &block)
|
149
150
|
end
|
151
|
+
|
152
|
+
service
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Synchronous version of DNSSD::Service#resolve
|
157
|
+
|
158
|
+
def self.resolve!(*args, &block)
|
159
|
+
service = DNSSD::Service.new
|
160
|
+
|
161
|
+
run(service, :resolve, *args, &block)
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Dispatches +args+ and +block+ to +method+ on +service+ and ensures
|
166
|
+
# +service+ is shut down after use.
|
167
|
+
|
168
|
+
def self.run(service, method, *args, &block)
|
169
|
+
service.send(method, *args, &block)
|
170
|
+
|
171
|
+
service
|
172
|
+
ensure
|
173
|
+
service.stop unless service.stopped?
|
150
174
|
end
|
175
|
+
|
151
176
|
end
|
152
177
|
|
153
|
-
|
178
|
+
require 'dnssd/flags'
|
179
|
+
require 'dnssd/reply'
|
180
|
+
require 'dnssd/service'
|
181
|
+
require 'dnssd/text_record'
|
182
|
+
|
data/lib/dnssd/flags.rb
CHANGED
@@ -22,6 +22,9 @@ class DNSSD::Flags
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
##
|
26
|
+
# Bitfield with all valid flags set
|
27
|
+
|
25
28
|
ALL_FLAGS = FLAGS.values.inject { |flag, all| flag | all }
|
26
29
|
|
27
30
|
##
|
@@ -71,16 +74,25 @@ class DNSSD::Flags
|
|
71
74
|
verify
|
72
75
|
end
|
73
76
|
|
77
|
+
##
|
78
|
+
# Returns an Array of flag names
|
79
|
+
|
74
80
|
def to_a
|
75
81
|
FLAGS.map do |name, value|
|
76
82
|
(@flags & value == value) ? name : nil
|
77
83
|
end.compact
|
78
84
|
end
|
79
85
|
|
80
|
-
|
86
|
+
##
|
87
|
+
# Flags as a bitfield
|
88
|
+
|
89
|
+
def to_i
|
81
90
|
@flags
|
82
91
|
end
|
83
92
|
|
93
|
+
##
|
94
|
+
# Trims the flag list down to valid flags
|
95
|
+
|
84
96
|
def verify
|
85
97
|
@flags &= ALL_FLAGS
|
86
98
|
|
data/lib/dnssd/reply.rb
CHANGED
@@ -29,7 +29,7 @@ class DNSSD::Reply
|
|
29
29
|
attr_reader :port
|
30
30
|
|
31
31
|
##
|
32
|
-
# The
|
32
|
+
# The DNSSD::Service associated with the reply
|
33
33
|
|
34
34
|
attr_reader :service
|
35
35
|
|
@@ -48,6 +48,9 @@ class DNSSD::Reply
|
|
48
48
|
|
49
49
|
attr_reader :type
|
50
50
|
|
51
|
+
##
|
52
|
+
# Creates a DNSSD::Reply from +service+ and +flags+
|
53
|
+
|
51
54
|
def self.from_service(service, flags)
|
52
55
|
reply = new
|
53
56
|
reply.instance_variable_set :@service, service
|
@@ -55,6 +58,52 @@ class DNSSD::Reply
|
|
55
58
|
reply
|
56
59
|
end
|
57
60
|
|
61
|
+
##
|
62
|
+
# Connects to this Reply. If +target+ and +port+ are missing, DNSSD.resolve
|
63
|
+
# is automatically called. +family+ can be used to select a particular
|
64
|
+
# address family.
|
65
|
+
|
66
|
+
def connect(family = Socket::AF_UNSPEC)
|
67
|
+
unless target and port then
|
68
|
+
value = nil
|
69
|
+
|
70
|
+
DNSSD.resolve! self do |reply|
|
71
|
+
value = reply
|
72
|
+
break
|
73
|
+
end
|
74
|
+
|
75
|
+
return value.connect
|
76
|
+
end
|
77
|
+
|
78
|
+
socktype = case protocol
|
79
|
+
when 'tcp' then Socket::SOCK_STREAM
|
80
|
+
when 'udp' then Socket::SOCK_DGRAM
|
81
|
+
else raise ArgumentError, "invalid protocol #{protocol}"
|
82
|
+
end
|
83
|
+
|
84
|
+
addresses = Socket.getaddrinfo target, port, family, socktype
|
85
|
+
|
86
|
+
socket = nil
|
87
|
+
|
88
|
+
addresses.each do |address|
|
89
|
+
begin
|
90
|
+
case protocol
|
91
|
+
when 'tcp' then
|
92
|
+
socket = TCPSocket.new address[3], port
|
93
|
+
when 'udp' then
|
94
|
+
socket = UDPSocket.new
|
95
|
+
socket.connect address[3], port rescue next
|
96
|
+
end
|
97
|
+
|
98
|
+
return socket
|
99
|
+
rescue
|
100
|
+
next
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
raise DNSSD::Error, "unable to connect to #{target}:#{port}" unless socket
|
105
|
+
end
|
106
|
+
|
58
107
|
##
|
59
108
|
# The full service domain name, see DNSS::Service#fullname
|
60
109
|
|
@@ -62,12 +111,29 @@ class DNSSD::Reply
|
|
62
111
|
DNSSD::Service.fullname @name.gsub("\032", ' '), @type, @domain
|
63
112
|
end
|
64
113
|
|
65
|
-
def inspect
|
114
|
+
def inspect # :nodoc:
|
66
115
|
"#<%s:0x%x %p type: %s domain: %s interface: %s flags: %s>" % [
|
67
116
|
self.class, object_id, @name, @type, @domain, @interface, @flags
|
68
117
|
]
|
69
118
|
end
|
70
119
|
|
120
|
+
##
|
121
|
+
# Protocol of this service
|
122
|
+
|
123
|
+
def protocol
|
124
|
+
type.split('.').last.sub '_', ''
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Service name as in Socket.getservbyname
|
129
|
+
|
130
|
+
def service_name
|
131
|
+
type.split('.').first.sub '_', ''
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Sets #name, #type and #domain from +fullname+
|
136
|
+
|
71
137
|
def set_fullname(fullname)
|
72
138
|
fullname = fullname.gsub(/\\([0-9]+)/) do $1.to_i.chr end
|
73
139
|
fullname = fullname.scan(/(?:[^\\.]|\\\.)+/).map do |part|
|
@@ -79,6 +145,9 @@ class DNSSD::Reply
|
|
79
145
|
@domain = fullname.last + '.'
|
80
146
|
end
|
81
147
|
|
148
|
+
##
|
149
|
+
# Sets #name, #type and #domain
|
150
|
+
|
82
151
|
def set_names(name, type, domain)
|
83
152
|
set_fullname [name, type, domain].join('.')
|
84
153
|
end
|
data/lib/dnssd/service.rb
CHANGED
@@ -1,15 +1,158 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
##
|
4
|
+
# A DNSSD::Service may be used for one DNS-SD call at a time. Between calls
|
5
|
+
# the service must be stopped. A single service can be reused multiple times.
|
6
|
+
#
|
7
|
+
# DNSSD::Service provides the raw DNS-SD functions via the _ variants.
|
8
|
+
|
1
9
|
class DNSSD::Service
|
2
10
|
|
3
11
|
##
|
4
|
-
#
|
5
|
-
|
12
|
+
# Creates a new DNSSD::Service
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@replies = []
|
16
|
+
@continue = true
|
17
|
+
@thread = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Browse for services.
|
22
|
+
#
|
23
|
+
# For each service found a DNSSD::Reply object is yielded.
|
24
|
+
#
|
25
|
+
# service = DNSSD::Service.new
|
26
|
+
# timeout 6 do
|
27
|
+
# service.browse '_http._tcp' do |r|
|
28
|
+
# puts "Found HTTP service: #{r.name}"
|
29
|
+
# end
|
30
|
+
# rescue Timeout::Error
|
31
|
+
# end
|
32
|
+
|
33
|
+
def browse(type, domain = nil, flags = 0, interface = DNSSD::InterfaceAny,
|
34
|
+
&block)
|
35
|
+
check_domain domain
|
36
|
+
interface = DNSSD.interface_index interface unless Integer === interface
|
37
|
+
|
38
|
+
raise DNSSD::Error, 'service in progress' if started?
|
39
|
+
|
40
|
+
_browse type, domain, flags.to_i, interface
|
41
|
+
|
42
|
+
process(&block)
|
43
|
+
end
|
6
44
|
|
7
|
-
|
45
|
+
##
|
46
|
+
# Raises an ArgumentError if +domain+ is too long including NULL terminator
|
47
|
+
# and trailing '.'
|
48
|
+
|
49
|
+
def check_domain(domain)
|
50
|
+
return unless domain
|
51
|
+
raise ArgumentError, 'domain name string is too long' if
|
52
|
+
domain.length >= MAX_DOMAIN_NAME - 1
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Enumerate domains available for browsing and registration.
|
57
|
+
#
|
58
|
+
# For each domain found a DNSSD::Reply object is passed to block with
|
59
|
+
# #domain set to the enumerated domain.
|
60
|
+
#
|
61
|
+
# available_domains = []
|
62
|
+
#
|
63
|
+
# timeout(2) do
|
64
|
+
# DNSSD.enumerate_domains! do |r|
|
65
|
+
# available_domains << r.domain
|
66
|
+
# end
|
67
|
+
# rescue TimeoutError
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# p available_domains
|
71
|
+
|
72
|
+
def enumerate_domains(flags = DNSSD::Flags::BrowseDomains,
|
73
|
+
interface = DNSSD::InterfaceAny, &block)
|
74
|
+
interface = DNSSD.interface_index interface unless Integer === interface
|
75
|
+
|
76
|
+
raise DNSSD::Error, 'service in progress' if started?
|
77
|
+
|
78
|
+
_enumerate_domains flags.to_i, interface
|
79
|
+
|
80
|
+
process(&block)
|
81
|
+
end
|
8
82
|
|
9
|
-
def inspect
|
83
|
+
def inspect # :nodoc:
|
10
84
|
stopped = stopped? ? 'stopped' : 'running'
|
11
85
|
"#<%s:0x%x %s>" % [self.class, object_id, stopped]
|
12
86
|
end
|
13
87
|
|
88
|
+
def process
|
89
|
+
@thread = Thread.current
|
90
|
+
|
91
|
+
while @continue do
|
92
|
+
_process if @replies.empty?
|
93
|
+
yield @replies.shift until @replies.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
@thread = nil
|
97
|
+
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Register a service. A DNSSD::Reply object is passed to the optional block
|
103
|
+
# when the registration completes.
|
104
|
+
#
|
105
|
+
# DNSSD.register! "My Files", "_http._tcp", nil, 8080 do |r|
|
106
|
+
# puts "successfully registered: #{r.inspect}"
|
107
|
+
# end
|
108
|
+
|
109
|
+
def register(name, type, domain, port, host = nil, text_record = nil,
|
110
|
+
flags = 0, interface = DNSSD::InterfaceAny, &block)
|
111
|
+
check_domain domain
|
112
|
+
interface = DNSSD.interface_index interface unless Integer === interface
|
113
|
+
|
114
|
+
raise DNSSD::Error, 'service in progress' if started?
|
115
|
+
|
116
|
+
_register name, type, domain, host, port, text_record, flags.to_i, interface
|
117
|
+
|
118
|
+
block = proc { } unless block
|
119
|
+
|
120
|
+
process(&block)
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Resolve a service discovered via #browse.
|
125
|
+
#
|
126
|
+
# +name+ may be either the name of the service found or a DNSSD::Reply from
|
127
|
+
# DNSSD::Service#browse. When +name+ is a DNSSD::Reply, +type+ and +domain+
|
128
|
+
# are automatically filled in, otherwise the service type and domain must be
|
129
|
+
# supplied.
|
130
|
+
#
|
131
|
+
# The service is resolved to a target host name, port number, and text
|
132
|
+
# record, all contained in the DNSSD::Reply object passed to the required
|
133
|
+
# block.
|
134
|
+
#
|
135
|
+
# The returned service can be used to control when to stop resolving the
|
136
|
+
# service (see DNSSD::Service#stop).
|
137
|
+
#
|
138
|
+
# s = DNSSD.resolve "foo bar", "_http._tcp", "local" do |r|
|
139
|
+
# p r
|
140
|
+
# end
|
141
|
+
# sleep 2
|
142
|
+
# s.stop
|
143
|
+
|
144
|
+
def resolve(name, type = name.type, domain = name.domain, flags = 0,
|
145
|
+
interface = DNSSD::InterfaceAny, &block)
|
146
|
+
name = name.name if DNSSD::Reply === name
|
147
|
+
check_domain domain
|
148
|
+
interface = DNSSD.interface_index interface unless Integer === interface
|
149
|
+
|
150
|
+
raise DNSSD::Error, 'service in progress' if started?
|
151
|
+
|
152
|
+
_resolve name, type, domain, flags.to_i, interface
|
153
|
+
|
154
|
+
process(&block)
|
155
|
+
end
|
156
|
+
|
14
157
|
end
|
15
158
|
|