cool.io 1.2.0-x86-mswin32-60
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/.gitignore +26 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CHANGES.md +177 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +172 -0
- data/Rakefile +81 -0
- data/cool.io.gemspec +28 -0
- data/examples/dslified_echo_client.rb +34 -0
- data/examples/dslified_echo_server.rb +24 -0
- data/examples/echo_client.rb +38 -0
- data/examples/echo_server.rb +27 -0
- data/examples/google.rb +9 -0
- data/examples/httpclient.rb +38 -0
- data/ext/cool.io/.gitignore +5 -0
- data/ext/cool.io/cool.io.h +58 -0
- data/ext/cool.io/cool.io_ext.c +25 -0
- data/ext/cool.io/ev_wrap.h +8 -0
- data/ext/cool.io/extconf.rb +73 -0
- data/ext/cool.io/iowatcher.c +189 -0
- data/ext/cool.io/libev.c +8 -0
- data/ext/cool.io/loop.c +301 -0
- data/ext/cool.io/stat_watcher.c +269 -0
- data/ext/cool.io/timer_watcher.c +219 -0
- data/ext/cool.io/utils.c +122 -0
- data/ext/cool.io/watcher.c +264 -0
- data/ext/cool.io/watcher.h +71 -0
- data/ext/http11_client/.gitignore +5 -0
- data/ext/http11_client/LICENSE +31 -0
- data/ext/http11_client/ext_help.h +14 -0
- data/ext/http11_client/extconf.rb +6 -0
- data/ext/http11_client/http11_client.c +300 -0
- data/ext/http11_client/http11_parser.c +403 -0
- data/ext/http11_client/http11_parser.h +48 -0
- data/ext/http11_client/http11_parser.rl +173 -0
- data/ext/iobuffer/extconf.rb +9 -0
- data/ext/iobuffer/iobuffer.c +765 -0
- data/ext/libev/Changes +388 -0
- data/ext/libev/LICENSE +36 -0
- data/ext/libev/README +58 -0
- data/ext/libev/README.embed +3 -0
- data/ext/libev/ev.c +4803 -0
- data/ext/libev/ev.h +845 -0
- data/ext/libev/ev_epoll.c +279 -0
- data/ext/libev/ev_kqueue.c +214 -0
- data/ext/libev/ev_poll.c +148 -0
- data/ext/libev/ev_port.c +185 -0
- data/ext/libev/ev_select.c +314 -0
- data/ext/libev/ev_vars.h +203 -0
- data/ext/libev/ev_win32.c +163 -0
- data/ext/libev/ev_wrap.h +200 -0
- data/ext/libev/test_libev_win32.c +123 -0
- data/lib/.gitignore +2 -0
- data/lib/cool.io.rb +32 -0
- data/lib/cool.io/async_watcher.rb +43 -0
- data/lib/cool.io/custom_require.rb +9 -0
- data/lib/cool.io/dns_resolver.rb +225 -0
- data/lib/cool.io/dsl.rb +135 -0
- data/lib/cool.io/eventmachine.rb +234 -0
- data/lib/cool.io/http_client.rb +427 -0
- data/lib/cool.io/io.rb +174 -0
- data/lib/cool.io/iowatcher.rb +17 -0
- data/lib/cool.io/listener.rb +93 -0
- data/lib/cool.io/loop.rb +130 -0
- data/lib/cool.io/meta.rb +49 -0
- data/lib/cool.io/server.rb +74 -0
- data/lib/cool.io/socket.rb +230 -0
- data/lib/cool.io/timer_watcher.rb +17 -0
- data/lib/cool.io/version.rb +5 -0
- data/lib/coolio.rb +2 -0
- data/spec/async_watcher_spec.rb +57 -0
- data/spec/dns_spec.rb +39 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/stat_watcher_spec.rb +77 -0
- data/spec/timer_watcher_spec.rb +55 -0
- data/spec/unix_listener_spec.rb +25 -0
- data/spec/unix_server_spec.rb +25 -0
- metadata +200 -0
data/lib/cool.io/io.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007-10 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
module Coolio
|
8
|
+
# A buffered I/O class witch fits into the Coolio Watcher framework.
|
9
|
+
# It provides both an observer which reads data as it's received
|
10
|
+
# from the wire and a buffered write watcher which stores data and writes
|
11
|
+
# it out each time the socket becomes writable.
|
12
|
+
#
|
13
|
+
# This class is primarily meant as a base class for other streams
|
14
|
+
# which need non-blocking writing, and is used to implement Coolio's
|
15
|
+
# Socket class and its associated subclasses.
|
16
|
+
class IO
|
17
|
+
extend Meta
|
18
|
+
|
19
|
+
# Maximum number of bytes to consume at once
|
20
|
+
INPUT_SIZE = 16384
|
21
|
+
|
22
|
+
def initialize(io)
|
23
|
+
@_io = io
|
24
|
+
@_write_buffer ||= ::IO::Buffer.new
|
25
|
+
@_read_watcher = Watcher.new(io, self, :r)
|
26
|
+
@_write_watcher = Watcher.new(io, self, :w)
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Watcher methods, delegated to @_read_watcher
|
31
|
+
#
|
32
|
+
|
33
|
+
# Attach to the event loop
|
34
|
+
def attach(loop); @_read_watcher.attach loop; schedule_write if !@_write_buffer.empty?; self; end
|
35
|
+
|
36
|
+
# Detach from the event loop
|
37
|
+
def detach; @_read_watcher.detach; self; end # TODO should these detect write buffers, as well?
|
38
|
+
|
39
|
+
# Enable the watcher
|
40
|
+
def enable; @_read_watcher.enable; self; end
|
41
|
+
|
42
|
+
# Disable the watcher
|
43
|
+
def disable; @_read_watcher.disable; self; end
|
44
|
+
|
45
|
+
# Is the watcher attached?
|
46
|
+
def attached?; @_read_watcher.attached?; end
|
47
|
+
|
48
|
+
# Is the watcher enabled?
|
49
|
+
def enabled?; @_read_watcher.enabled?; end
|
50
|
+
|
51
|
+
# Obtain the event loop associated with this object
|
52
|
+
def evloop; @_read_watcher.evloop; end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Callbacks for asynchronous events
|
56
|
+
#
|
57
|
+
|
58
|
+
# Called whenever the IO object receives data
|
59
|
+
def on_read(data); end
|
60
|
+
event_callback :on_read
|
61
|
+
|
62
|
+
# Called whenever a write completes and the output buffer is empty
|
63
|
+
def on_write_complete; end
|
64
|
+
event_callback :on_write_complete
|
65
|
+
|
66
|
+
# Called whenever the IO object hits EOF
|
67
|
+
def on_close; end
|
68
|
+
event_callback :on_close
|
69
|
+
|
70
|
+
#
|
71
|
+
# Write interface
|
72
|
+
#
|
73
|
+
|
74
|
+
# Write data in a buffered, non-blocking manner
|
75
|
+
def write(data)
|
76
|
+
@_write_buffer << data
|
77
|
+
schedule_write
|
78
|
+
data.size
|
79
|
+
end
|
80
|
+
|
81
|
+
# Number of bytes are currently in the output buffer
|
82
|
+
def output_buffer_size
|
83
|
+
@_write_buffer.size
|
84
|
+
end
|
85
|
+
|
86
|
+
# Close the IO stream
|
87
|
+
def close
|
88
|
+
detach if attached?
|
89
|
+
detach_write_watcher
|
90
|
+
@_io.close unless @_io.closed?
|
91
|
+
|
92
|
+
on_close
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
# Is the IO object closed?
|
97
|
+
def closed?
|
98
|
+
@_io.nil? or @_io.closed?
|
99
|
+
end
|
100
|
+
|
101
|
+
#########
|
102
|
+
protected
|
103
|
+
#########
|
104
|
+
|
105
|
+
# Read from the input buffer and dispatch to on_read
|
106
|
+
def on_readable
|
107
|
+
begin
|
108
|
+
on_read @_io.read_nonblock(INPUT_SIZE)
|
109
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
110
|
+
return
|
111
|
+
|
112
|
+
# SystemCallError catches Errno::ECONNRESET amongst others.
|
113
|
+
rescue SystemCallError, EOFError, IOError, SocketError
|
114
|
+
close
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Write the contents of the output buffer
|
119
|
+
def on_writable
|
120
|
+
begin
|
121
|
+
@_write_buffer.write_to(@_io)
|
122
|
+
rescue Errno::EINTR
|
123
|
+
return
|
124
|
+
|
125
|
+
# SystemCallError catches Errno::EPIPE & Errno::ECONNRESET amongst others.
|
126
|
+
rescue SystemCallError, IOError, SocketError
|
127
|
+
return close
|
128
|
+
end
|
129
|
+
|
130
|
+
if @_write_buffer.empty?
|
131
|
+
disable_write_watcher
|
132
|
+
on_write_complete
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Schedule a write to be performed when the IO object becomes writable
|
137
|
+
def schedule_write
|
138
|
+
return unless @_io # this would mean 'we are still pre DNS here'
|
139
|
+
return unless attached? # this would mean 'currently unattached' -- ie still pre DNS, or just plain not attached, which is ok
|
140
|
+
begin
|
141
|
+
enable_write_watcher
|
142
|
+
rescue IOError
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def enable_write_watcher
|
147
|
+
if @_write_watcher.attached?
|
148
|
+
@_write_watcher.enable unless @_write_watcher.enabled?
|
149
|
+
else
|
150
|
+
@_write_watcher.attach(evloop)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def disable_write_watcher
|
155
|
+
@_write_watcher.disable if @_write_watcher and @_write_watcher.enabled?
|
156
|
+
end
|
157
|
+
|
158
|
+
def detach_write_watcher
|
159
|
+
@_write_watcher.detach if @_write_watcher and @_write_watcher.attached?
|
160
|
+
end
|
161
|
+
|
162
|
+
# Internal class implementing watchers used by Coolio::IO
|
163
|
+
class Watcher < IOWatcher
|
164
|
+
def initialize(ruby_io, coolio_io, flags)
|
165
|
+
@coolio_io = coolio_io
|
166
|
+
super(ruby_io, flags)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Configure IOWatcher event callbacks to call the method passed to #initialize
|
170
|
+
def on_readable; @coolio_io.__send__(:on_readable); end
|
171
|
+
def on_writable; @coolio_io.__send__(:on_writable); end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007-10 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
module Coolio
|
8
|
+
class IOWatcher
|
9
|
+
# The actual implementation of this class resides in the C extension
|
10
|
+
# Here we metaprogram proper event_callbacks for the callback methods
|
11
|
+
# These can take a block and store it to be called when the event
|
12
|
+
# is actually fired.
|
13
|
+
|
14
|
+
extend Meta
|
15
|
+
event_callback :on_readable, :on_writable
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007-10 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'socket'
|
8
|
+
|
9
|
+
module Coolio
|
10
|
+
# Listeners wait for incoming connections. When a listener receives a
|
11
|
+
# connection it fires the on_connection event with the newly accepted
|
12
|
+
# socket as a parameter.
|
13
|
+
class Listener < IOWatcher
|
14
|
+
def initialize(listen_socket)
|
15
|
+
@listen_socket = listen_socket
|
16
|
+
super(@listen_socket)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an integer representing the underlying numeric file descriptor
|
20
|
+
def fileno
|
21
|
+
@listen_socket.fileno
|
22
|
+
end
|
23
|
+
|
24
|
+
# Close the listener
|
25
|
+
def close
|
26
|
+
detach if attached?
|
27
|
+
@listen_socket.close
|
28
|
+
end
|
29
|
+
|
30
|
+
# Called whenever the server receives a new connection
|
31
|
+
def on_connection(socket); end
|
32
|
+
event_callback :on_connection
|
33
|
+
|
34
|
+
#########
|
35
|
+
protected
|
36
|
+
#########
|
37
|
+
|
38
|
+
# Coolio callback for handling new connections
|
39
|
+
def on_readable
|
40
|
+
begin
|
41
|
+
on_connection @listen_socket.accept_nonblock
|
42
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED
|
43
|
+
# EAGAIN can be triggered here if the socket is shared between
|
44
|
+
# multiple processes and a thundering herd is woken up to accept
|
45
|
+
# one connection, only one process will get the connection and
|
46
|
+
# the others will be awoken.
|
47
|
+
# ECONNABORTED is documented in accept() manpages but modern TCP
|
48
|
+
# stacks with syncookies and/or accept()-filtering for DoS
|
49
|
+
# protection do not see it. In any case this error is harmless
|
50
|
+
# and we should instead spend our time with clients that follow
|
51
|
+
# through on connection attempts.
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class TCPListener < Listener
|
57
|
+
DEFAULT_BACKLOG = 1024
|
58
|
+
|
59
|
+
# Create a new Coolio::TCPListener on the specified address and port.
|
60
|
+
# Accepts the following options:
|
61
|
+
#
|
62
|
+
# :backlog - Max size of the pending connection queue (default 1024)
|
63
|
+
# :reverse_lookup - Retain BasicSocket's reverse DNS functionality (default false)
|
64
|
+
#
|
65
|
+
# If the specified address is an TCPServer object, it will ignore
|
66
|
+
# the port and :backlog option and create a new Coolio::TCPListener out
|
67
|
+
# of the existing TCPServer object.
|
68
|
+
def initialize(addr, port = nil, options = {})
|
69
|
+
BasicSocket.do_not_reverse_lookup = true unless options[:reverse_lookup]
|
70
|
+
options[:backlog] ||= DEFAULT_BACKLOG
|
71
|
+
|
72
|
+
listen_socket = if ::TCPServer === addr
|
73
|
+
addr
|
74
|
+
else
|
75
|
+
raise ArgumentError, "port must be an integer" if nil == port
|
76
|
+
::TCPServer.new(addr, port)
|
77
|
+
end
|
78
|
+
listen_socket.instance_eval { listen(options[:backlog]) }
|
79
|
+
super(listen_socket)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class UNIXListener < Listener
|
84
|
+
# Create a new Coolio::UNIXListener
|
85
|
+
#
|
86
|
+
# Accepts the same arguments as UNIXServer.new
|
87
|
+
# Optionally, it can also take anyn existing UNIXServer object
|
88
|
+
# and create a Coolio::UNIXListener out of it.
|
89
|
+
def initialize(*args)
|
90
|
+
super(::UNIXServer === args.first ? args.first : ::UNIXServer.new(*args))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/cool.io/loop.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007-10 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'thread'
|
8
|
+
|
9
|
+
# Monkeypatch Thread to include a method for obtaining the default Coolio::Loop
|
10
|
+
class Thread
|
11
|
+
def _coolio_loop
|
12
|
+
@_coolio_loop ||= Coolio::Loop.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Coolio
|
17
|
+
class Loop
|
18
|
+
# In Ruby 1.9 we want a Coolio::Loop per thread, but Ruby 1.8 is unithreaded
|
19
|
+
if RUBY_VERSION >= "1.9.0"
|
20
|
+
# Retrieve the default event loop for the current thread
|
21
|
+
def self.default
|
22
|
+
Thread.current._coolio_loop
|
23
|
+
end
|
24
|
+
else
|
25
|
+
# Retrieve the default event loop
|
26
|
+
def self.default
|
27
|
+
@@_coolio_loop ||= Coolio::Loop.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a new Coolio::Loop
|
32
|
+
#
|
33
|
+
# Options:
|
34
|
+
#
|
35
|
+
# :skip_environment (boolean)
|
36
|
+
# Ignore the $LIBEV_FLAGS environment variable
|
37
|
+
#
|
38
|
+
# :fork_check (boolean)
|
39
|
+
# Enable autodetection of forks
|
40
|
+
#
|
41
|
+
# :backend
|
42
|
+
# Choose the default backend, one (or many in an array) of:
|
43
|
+
# :select (most platforms)
|
44
|
+
# :poll (most platforms except Windows)
|
45
|
+
# :epoll (Linux)
|
46
|
+
# :kqueue (BSD/Mac OS X)
|
47
|
+
# :port (Solaris 10)
|
48
|
+
#
|
49
|
+
def initialize(options = {})
|
50
|
+
@watchers = {}
|
51
|
+
@active_watchers = 0
|
52
|
+
|
53
|
+
flags = 0
|
54
|
+
|
55
|
+
options.each do |option, value|
|
56
|
+
case option
|
57
|
+
when :skip_environment
|
58
|
+
flags |= EVFLAG_NOEV if value
|
59
|
+
when :fork_check
|
60
|
+
flags |= EVFLAG_FORKCHECK if value
|
61
|
+
when :backend
|
62
|
+
value = [value] unless value.is_a? Array
|
63
|
+
value.each do |backend|
|
64
|
+
case backend
|
65
|
+
when :select then flags |= EVBACKEND_SELECT
|
66
|
+
when :poll then flags |= EVBACKEND_POLL
|
67
|
+
when :epoll then flags |= EVBACKEND_EPOLL
|
68
|
+
when :kqueue then flags |= EVBACKEND_KQUEUE
|
69
|
+
when :port then flags |= EVBACKEND_PORT
|
70
|
+
else raise ArgumentError, "no such backend: #{backend}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
else raise ArgumentError, "no such option: #{option}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
@loop = ev_loop_new(flags)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Attach a watcher to the loop
|
81
|
+
def attach(watcher)
|
82
|
+
watcher.attach self
|
83
|
+
end
|
84
|
+
|
85
|
+
# Run the event loop and dispatch events back to Ruby. If there
|
86
|
+
# are no watchers associated with the event loop it will return
|
87
|
+
# immediately. Otherwise, run will continue blocking and making
|
88
|
+
# event callbacks to watchers until all watchers associated with
|
89
|
+
# the loop have been disabled or detached. The loop may be
|
90
|
+
# explicitly stopped by calling the stop method on the loop object.
|
91
|
+
def run
|
92
|
+
raise RuntimeError, "no watchers for this loop" if @watchers.empty?
|
93
|
+
|
94
|
+
@running = true
|
95
|
+
while @running and not @active_watchers.zero?
|
96
|
+
run_once
|
97
|
+
end
|
98
|
+
@running = false
|
99
|
+
end
|
100
|
+
|
101
|
+
# Stop the event loop if it's running
|
102
|
+
def stop
|
103
|
+
raise RuntimeError, "loop not running" unless @running
|
104
|
+
@running = false
|
105
|
+
end
|
106
|
+
|
107
|
+
# Does the loop have any active watchers?
|
108
|
+
def has_active_watchers?
|
109
|
+
@active_watchers > 0
|
110
|
+
end
|
111
|
+
|
112
|
+
# All watchers attached to the current loop
|
113
|
+
def watchers
|
114
|
+
@watchers.keys
|
115
|
+
end
|
116
|
+
|
117
|
+
#######
|
118
|
+
private
|
119
|
+
#######
|
120
|
+
|
121
|
+
EVFLAG_NOENV = 0x1000000 # do NOT consult environment
|
122
|
+
EVFLAG_FORKCHECK = 0x2000000 # check for a fork in each iteration
|
123
|
+
|
124
|
+
EVBACKEND_SELECT = 0x00000001 # supported about anywhere
|
125
|
+
EVBACKEND_POLL = 0x00000002 # !win
|
126
|
+
EVBACKEND_EPOLL = 0x00000004 # linux
|
127
|
+
EVBACKEND_KQUEUE = 0x00000008 # bsd
|
128
|
+
EVBACKEND_PORT = 0x00000020 # solaris 10
|
129
|
+
end
|
130
|
+
end
|
data/lib/cool.io/meta.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007-10 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
module Coolio
|
8
|
+
module Meta
|
9
|
+
# Use an alternate watcher with the attach/detach/enable/disable methods
|
10
|
+
# if it is presently assigned. This is useful if you are waiting for
|
11
|
+
# an event to occur before the current watcher can be used in earnest,
|
12
|
+
# such as making an outgoing TCP connection.
|
13
|
+
def watcher_delegate(proxy_var)
|
14
|
+
%w{attach detach enable disable}.each do |method|
|
15
|
+
module_eval <<-EOD
|
16
|
+
def #{method}(*args)
|
17
|
+
if defined? #{proxy_var} and #{proxy_var}
|
18
|
+
#{proxy_var}.#{method}(*args)
|
19
|
+
return self
|
20
|
+
end
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
EOD
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Define callbacks whose behavior can be changed on-the-fly per instance.
|
29
|
+
# This is done by giving a block to the callback method, which is captured
|
30
|
+
# as a proc and stored for later. If the method is called without a block,
|
31
|
+
# the stored block is executed if present, otherwise it's a noop.
|
32
|
+
def event_callback(*methods)
|
33
|
+
methods.each do |method|
|
34
|
+
module_eval <<-EOD
|
35
|
+
def #{method}(*args, &block)
|
36
|
+
if block
|
37
|
+
@#{method}_callback = block
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
if defined? @#{method}_callback and @#{method}_callback
|
42
|
+
instance_exec(*args, &@#{method}_callback)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
EOD
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|