eventmachine-win32 0.5.3 → 0.7.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.
- data/RELEASE_NOTES +14 -1
- data/TODO +10 -0
- data/lib/em/deferrable.rb +89 -0
- data/lib/em/eventable.rb +50 -0
- data/lib/eventmachine.rb +274 -9
- data/lib/eventmachine_version.rb +42 -0
- data/lib/evma.rb +35 -0
- data/lib/evma/callback.rb +35 -0
- data/lib/evma/container.rb +77 -0
- data/lib/evma/factory.rb +80 -0
- data/lib/evma/protocol.rb +89 -0
- data/lib/evma/reactor.rb +50 -0
- data/lib/pr_eventmachine.rb +714 -0
- data/lib/protocols/header_and_content.rb +134 -0
- data/lib/protocols/httpclient.rb +233 -0
- data/lib/protocols/line_and_text.rb +141 -0
- data/lib/protocols/tcptest.rb +67 -0
- data/lib/rubyeventmachine.so +0 -0
- data/tests/test_basic.rb +106 -0
- data/tests/test_eventables.rb +85 -0
- data/tests/test_hc.rb +207 -0
- data/tests/test_httpclient.rb +94 -0
- data/tests/test_ltp.rb +192 -0
- data/tests/test_ud.rb +52 -0
- metadata +29 -3
@@ -0,0 +1,134 @@
|
|
1
|
+
# $Id: header_and_content.rb 281 2006-11-20 03:17:22Z blackhedd $
|
2
|
+
#
|
3
|
+
# Author:: blackhedd (gmail address: garbagecat10).
|
4
|
+
# Date:: 15 November 2006
|
5
|
+
#
|
6
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
7
|
+
#
|
8
|
+
# This program is made available under the terms of the GPL version 2.
|
9
|
+
#
|
10
|
+
# See EventMachine and EventMachine::Connection for documentation and
|
11
|
+
# usage examples.
|
12
|
+
#
|
13
|
+
#----------------------------------------------------------------------------
|
14
|
+
#
|
15
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
16
|
+
#
|
17
|
+
# Gmail: garbagecat10
|
18
|
+
#
|
19
|
+
# This program is free software; you can redistribute it and/or modify
|
20
|
+
# it under the terms of the GNU General Public License as published by
|
21
|
+
# the Free Software Foundation; either version 2 of the License, or
|
22
|
+
# (at your option) any later version.
|
23
|
+
#
|
24
|
+
# This program is distributed in the hope that it will be useful,
|
25
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
26
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
27
|
+
# GNU General Public License for more details.
|
28
|
+
#
|
29
|
+
# You should have received a copy of the GNU General Public License
|
30
|
+
# along with this program; if not, write to the Free Software
|
31
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
32
|
+
#
|
33
|
+
#---------------------------------------------------------------------------
|
34
|
+
#
|
35
|
+
#
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
module EventMachine
|
40
|
+
module Protocols
|
41
|
+
|
42
|
+
class HeaderAndContentProtocol < LineAndTextProtocol
|
43
|
+
|
44
|
+
ContentLengthPattern = /Content-length:\s*(\d+)/i
|
45
|
+
|
46
|
+
def initialize *args
|
47
|
+
super
|
48
|
+
init_for_request
|
49
|
+
end
|
50
|
+
|
51
|
+
def receive_line line
|
52
|
+
case @hc_mode
|
53
|
+
when :discard_blanks
|
54
|
+
unless line == ""
|
55
|
+
@hc_mode = :headers
|
56
|
+
receive_line line
|
57
|
+
end
|
58
|
+
when :headers
|
59
|
+
if line == ""
|
60
|
+
raise "unrecognized state" unless @hc_headers.length > 0
|
61
|
+
if respond_to?(:receive_headers)
|
62
|
+
receive_headers @hc_headers
|
63
|
+
end
|
64
|
+
# @hc_content_length will be nil, not 0, if there was no content-length header.
|
65
|
+
if @hc_content_length.to_i > 0
|
66
|
+
set_binary_mode @hc_content_length
|
67
|
+
else
|
68
|
+
dispatch_request
|
69
|
+
end
|
70
|
+
else
|
71
|
+
@hc_headers << line
|
72
|
+
if ContentLengthPattern =~ line
|
73
|
+
# There are some attacks that rely on sending multiple content-length
|
74
|
+
# headers. This is a crude protection, but needs to become tunable.
|
75
|
+
raise "extraneous content-length header" if @hc_content_length
|
76
|
+
@hc_content_length = $1.to_i
|
77
|
+
end
|
78
|
+
if @hc_headers.length == 1 and respond_to?(:receive_first_header_line)
|
79
|
+
receive_first_header_line line
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
raise "internal error, unsupported mode"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def receive_binary_data text
|
88
|
+
@hc_content = text
|
89
|
+
dispatch_request
|
90
|
+
end
|
91
|
+
|
92
|
+
def dispatch_request
|
93
|
+
if respond_to?(:receive_request)
|
94
|
+
receive_request @hc_headers, @hc_content
|
95
|
+
end
|
96
|
+
init_for_request
|
97
|
+
end
|
98
|
+
private :dispatch_request
|
99
|
+
|
100
|
+
def init_for_request
|
101
|
+
@hc_mode = :discard_blanks
|
102
|
+
@hc_headers = []
|
103
|
+
# originally was @hc_headers ||= []; @hc_headers.clear to get a performance
|
104
|
+
# boost, but it's counterproductive because a subclassed handler will have to
|
105
|
+
# call dup to use the header array we pass in receive_headers.
|
106
|
+
|
107
|
+
@hc_content_length = nil
|
108
|
+
@hc_content = ""
|
109
|
+
end
|
110
|
+
private :init_for_request
|
111
|
+
|
112
|
+
# Basically a convenience method. We might create a subclass that does this
|
113
|
+
# automatically. But it's such a performance killer.
|
114
|
+
def headers_2_hash hdrs
|
115
|
+
self.class.headers_2_hash hdrs
|
116
|
+
end
|
117
|
+
|
118
|
+
class << self
|
119
|
+
def headers_2_hash hdrs
|
120
|
+
hash = {}
|
121
|
+
hdrs.each {|h|
|
122
|
+
if /\A([^\s:]+)\s*:\s*/ =~ h
|
123
|
+
tail = $'.dup
|
124
|
+
hash[ $1.downcase.gsub(/-/,"_").intern ] = tail
|
125
|
+
end
|
126
|
+
}
|
127
|
+
hash
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# $Id: httpclient.rb 226 2006-08-10 08:55:49Z blackhedd $
|
2
|
+
#
|
3
|
+
# Author:: blackhedd (gmail address: garbagecat10).
|
4
|
+
# Date:: 16 July 2006
|
5
|
+
#
|
6
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
7
|
+
#
|
8
|
+
# This program is made available under the terms of the GPL version 2.
|
9
|
+
#
|
10
|
+
# See EventMachine and EventMachine::Connection for documentation and
|
11
|
+
# usage examples.
|
12
|
+
#
|
13
|
+
#----------------------------------------------------------------------------
|
14
|
+
#
|
15
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
16
|
+
#
|
17
|
+
# Gmail: garbagecat10
|
18
|
+
#
|
19
|
+
# This program is free software; you can redistribute it and/or modify
|
20
|
+
# it under the terms of the GNU General Public License as published by
|
21
|
+
# the Free Software Foundation; either version 2 of the License, or
|
22
|
+
# (at your option) any later version.
|
23
|
+
#
|
24
|
+
# This program is distributed in the hope that it will be useful,
|
25
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
26
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
27
|
+
# GNU General Public License for more details.
|
28
|
+
#
|
29
|
+
# You should have received a copy of the GNU General Public License
|
30
|
+
# along with this program; if not, write to the Free Software
|
31
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
32
|
+
#
|
33
|
+
#---------------------------------------------------------------------------
|
34
|
+
#
|
35
|
+
#
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
module EventMachine
|
40
|
+
module Protocols
|
41
|
+
|
42
|
+
class HttpClient < Connection
|
43
|
+
include EventMachine::Deferrable
|
44
|
+
|
45
|
+
|
46
|
+
# USAGE SAMPLE:
|
47
|
+
# WARNING, POST is not yet supported!!!!!
|
48
|
+
#
|
49
|
+
# EventMachine.run {
|
50
|
+
# http = EventMachine::Protocols::HttpClient.request(
|
51
|
+
# :host => server,
|
52
|
+
# :port => 80,
|
53
|
+
# :request => "/index.html",
|
54
|
+
# :query_string => "parm1=value1&parm2=value2"
|
55
|
+
# )
|
56
|
+
# http.callback {|response|
|
57
|
+
# puts response[:status]
|
58
|
+
# puts response[:headers]
|
59
|
+
# puts response[:content]
|
60
|
+
# }
|
61
|
+
# }
|
62
|
+
#
|
63
|
+
|
64
|
+
# TODO:
|
65
|
+
# POST REQUESTS!!!!!!!!!!!
|
66
|
+
# Timeout for connections that run too long or hang somewhere in the middle.
|
67
|
+
# Persistent connections (HTTP/1.1), may need a associated delegate object.
|
68
|
+
# DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
|
69
|
+
# DNS lookups are unbelievably slow.
|
70
|
+
# HEAD requests.
|
71
|
+
# Chunked transfer encoding.
|
72
|
+
# Convenience methods for requests. get, post, url, etc.
|
73
|
+
# SSL.
|
74
|
+
# Handle status codes like 304, 100, etc.
|
75
|
+
# Refactor this code so that protocol errors all get handled one way (an exception?),
|
76
|
+
# instead of sprinkling set_deferred_status :failed calls everywhere.
|
77
|
+
|
78
|
+
def self.request( args = {} )
|
79
|
+
args[:port] ||= 80
|
80
|
+
EventMachine.connect( args[:host], args[:port], self ) {|c|
|
81
|
+
# According to the docs, we will get here AFTER post_init is called.
|
82
|
+
c.instance_eval {@args = args}
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def post_init
|
87
|
+
@start_time = Time.now
|
88
|
+
@data = ""
|
89
|
+
@read_state = :base
|
90
|
+
end
|
91
|
+
|
92
|
+
# We send the request when we get a connection.
|
93
|
+
# AND, we set an instance variable to indicate we passed through here.
|
94
|
+
# That allows #unbind to know whether there was a successful connection.
|
95
|
+
# NB: This naive technique won't work when we have to support multiple
|
96
|
+
# requests on a single connection.
|
97
|
+
def connection_completed
|
98
|
+
@connected = true
|
99
|
+
send_request @args
|
100
|
+
end
|
101
|
+
|
102
|
+
def send_request args
|
103
|
+
args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
|
104
|
+
|
105
|
+
verb = args[:verb].to_s.upcase
|
106
|
+
unless ["GET", "POST", "HEAD"].include?(verb)
|
107
|
+
set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
|
108
|
+
return # NOTE THE EARLY RETURN, we're not sending any data.
|
109
|
+
end
|
110
|
+
|
111
|
+
request = args[:request] || "/"
|
112
|
+
unless request[0,1] == "/"
|
113
|
+
request = "/" + request
|
114
|
+
end
|
115
|
+
|
116
|
+
qs = args[:query_string] || ""
|
117
|
+
if qs.length > 0 and qs[0,1] != '?'
|
118
|
+
qs = "?" + qs
|
119
|
+
end
|
120
|
+
|
121
|
+
# Allow an override for the host header if it's not the connect-string.
|
122
|
+
host = args[:host_header] || args[:host] || "_"
|
123
|
+
|
124
|
+
# ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
|
125
|
+
req = [
|
126
|
+
"#{verb} #{request}#{qs} HTTP/1.1",
|
127
|
+
"Host: #{host}",
|
128
|
+
"User-agent: Ruby EventMachine",
|
129
|
+
""
|
130
|
+
].map {|l| "#{l}\r\n"}.join
|
131
|
+
|
132
|
+
send_data req
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def receive_data data
|
137
|
+
while data and data.length > 0
|
138
|
+
case @read_state
|
139
|
+
when :base
|
140
|
+
# Perform any per-request initialization here and don't consume any data.
|
141
|
+
@data = ""
|
142
|
+
@headers = []
|
143
|
+
@content_length = nil # not zero
|
144
|
+
@content = ""
|
145
|
+
@status = nil
|
146
|
+
@read_state = :header
|
147
|
+
when :header
|
148
|
+
ary = data.split( /\r?\n/m, 2 )
|
149
|
+
if ary.length == 2
|
150
|
+
data = ary.last
|
151
|
+
if ary.first == ""
|
152
|
+
@read_state = :content
|
153
|
+
else
|
154
|
+
@headers << ary.first
|
155
|
+
if @headers.length == 1
|
156
|
+
parse_response_line
|
157
|
+
elsif ary.first =~ /\Acontent-length:\s*/i
|
158
|
+
# Only take the FIRST content-length header that appears,
|
159
|
+
# which we can distinguish because @content_length is nil.
|
160
|
+
# TODO, it's actually a fatal error if there is more than one
|
161
|
+
# content-length header, because the caller is presumptively
|
162
|
+
# a bad guy. (There is an exploit that depends on multiple
|
163
|
+
# content-length headers.)
|
164
|
+
@content_length ||= $'.to_i
|
165
|
+
end
|
166
|
+
end
|
167
|
+
else
|
168
|
+
@data << data
|
169
|
+
data = ""
|
170
|
+
end
|
171
|
+
when :content
|
172
|
+
# If there was no content-length header, we have to wait until the connection
|
173
|
+
# closes. Everything we get until that point is content.
|
174
|
+
# TODO: Must impose a content-size limit, and also must implement chunking.
|
175
|
+
# Also, must support either temporary files for large content, or calling
|
176
|
+
# a content-consumer block supplied by the user.
|
177
|
+
if @content_length
|
178
|
+
bytes_needed = @content_length - @content.length
|
179
|
+
@content += data[0, bytes_needed]
|
180
|
+
data = data[bytes_needed..-1] || ""
|
181
|
+
if @content_length == @content.length
|
182
|
+
dispatch_response
|
183
|
+
@read_state = :base
|
184
|
+
end
|
185
|
+
else
|
186
|
+
@content << data
|
187
|
+
data = ""
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
# We get called here when we have received an HTTP response line.
|
195
|
+
# It's an opportunity to throw an exception or trigger other exceptional
|
196
|
+
# handling.
|
197
|
+
def parse_response_line
|
198
|
+
if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
|
199
|
+
@status = $1.to_i
|
200
|
+
else
|
201
|
+
set_deferred_status :failed, {
|
202
|
+
:status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
|
203
|
+
}
|
204
|
+
close_connection
|
205
|
+
end
|
206
|
+
end
|
207
|
+
private :parse_response_line
|
208
|
+
|
209
|
+
def dispatch_response
|
210
|
+
@read_state = :base
|
211
|
+
set_deferred_status :succeeded, {
|
212
|
+
:content => @content,
|
213
|
+
:headers => @headers,
|
214
|
+
:status => @status
|
215
|
+
}
|
216
|
+
# TODO, we close the connection for now, but this is wrong for persistent clients.
|
217
|
+
close_connection
|
218
|
+
end
|
219
|
+
|
220
|
+
def unbind
|
221
|
+
if !@connected
|
222
|
+
set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
|
223
|
+
elsif (@read_state == :content and @content_length == nil)
|
224
|
+
dispatch_response
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# $Id: line_and_text.rb 279 2006-11-18 15:40:25Z blackhedd $
|
2
|
+
#
|
3
|
+
# Author:: blackhedd (gmail address: garbagecat10).
|
4
|
+
# Date:: 15 November 2006
|
5
|
+
#
|
6
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
7
|
+
#
|
8
|
+
# This program is made available under the terms of the GPL version 2.
|
9
|
+
#
|
10
|
+
# See EventMachine and EventMachine::Connection for documentation and
|
11
|
+
# usage examples.
|
12
|
+
#
|
13
|
+
#----------------------------------------------------------------------------
|
14
|
+
#
|
15
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
16
|
+
#
|
17
|
+
# Gmail: garbagecat10
|
18
|
+
#
|
19
|
+
# This program is free software; you can redistribute it and/or modify
|
20
|
+
# it under the terms of the GNU General Public License as published by
|
21
|
+
# the Free Software Foundation; either version 2 of the License, or
|
22
|
+
# (at your option) any later version.
|
23
|
+
#
|
24
|
+
# This program is distributed in the hope that it will be useful,
|
25
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
26
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
27
|
+
# GNU General Public License for more details.
|
28
|
+
#
|
29
|
+
# You should have received a copy of the GNU General Public License
|
30
|
+
# along with this program; if not, write to the Free Software
|
31
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
32
|
+
#
|
33
|
+
#---------------------------------------------------------------------------
|
34
|
+
#
|
35
|
+
#
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
module EventMachine
|
40
|
+
module Protocols
|
41
|
+
|
42
|
+
class LineAndTextProtocol < Connection
|
43
|
+
MaxLineLength = 16*1024
|
44
|
+
MaxBinaryLength = 32*1024*1024
|
45
|
+
|
46
|
+
def initialize *args
|
47
|
+
super
|
48
|
+
lbp_init_line_state
|
49
|
+
end
|
50
|
+
def receive_data data
|
51
|
+
if @lbp_mode == :lines
|
52
|
+
@lbp_data << data
|
53
|
+
while i = @lbp_data.index("\n")
|
54
|
+
# line-length test is provisional. Need to be tunable and do something
|
55
|
+
# more intelligent than throwing something.
|
56
|
+
if i > MaxLineLength
|
57
|
+
receive_error("overlength line") if respond_to?(:receive_error)
|
58
|
+
close_connection
|
59
|
+
break # exit the while loop
|
60
|
+
end
|
61
|
+
line = @lbp_data.slice!(0..i).chomp
|
62
|
+
receive_line line if respond_to?(:receive_line)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
if @lbp_binary_limit > 0
|
66
|
+
wanted = @lbp_binary_limit - @lbp_binary_bytes_received
|
67
|
+
chunk = nil
|
68
|
+
if data.length > wanted
|
69
|
+
chunk = data.slice!(0...wanted)
|
70
|
+
else
|
71
|
+
chunk = data
|
72
|
+
data = ""
|
73
|
+
end
|
74
|
+
@lbp_binary_buffer[@lbp_binary_bytes_received...(@lbp_binary_bytes_received+chunk.length)] = chunk
|
75
|
+
@lbp_binary_bytes_received += chunk.length
|
76
|
+
if @lbp_binary_bytes_received == @lbp_binary_limit
|
77
|
+
receive_binary_data(@lbp_binary_buffer) if respond_to?(:receive_binary_data)
|
78
|
+
lbp_init_line_state
|
79
|
+
end
|
80
|
+
receive_data(data) if data.length > 0
|
81
|
+
else
|
82
|
+
receive_binary_data(data) if respond_to?(:receive_binary_data)
|
83
|
+
data = ""
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def unbind
|
89
|
+
if @lbp_mode == :binary and @lbp_binary_limit > 0
|
90
|
+
if respond_to?(:receive_binary_data)
|
91
|
+
receive_binary_data( @lbp_binary_buffer[0...@lbp_binary_bytes_received] )
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Set up to read the supplied number of binary bytes.
|
97
|
+
# This recycles all the data currently waiting in the line buffer, if any.
|
98
|
+
# If the limit is nil, then ALL subsequent data will be treated as binary
|
99
|
+
# data and passed to the upstream protocol handler as we receive it.
|
100
|
+
# If a limit is given, we'll hold the incoming binary data and not
|
101
|
+
# pass it upstream until we've seen it all, or until there is an unbind
|
102
|
+
# (in which case we'll pass up a partial).
|
103
|
+
# Specifying nil for the limit (the default) means there is no limit.
|
104
|
+
# Specifiyng zero for the limit will cause an immediate transition back to line mode.
|
105
|
+
#
|
106
|
+
def set_binary_mode size = nil
|
107
|
+
if @lbp_mode == :lines
|
108
|
+
if size == 0
|
109
|
+
receive_binary_data("") if respond_to?(:receive_binary_data)
|
110
|
+
# Do no more work here. Stay in line mode and keep consuming data.
|
111
|
+
else
|
112
|
+
@lbp_binary_limit = size.to_i # (nil will be stored as zero)
|
113
|
+
if @lbp_binary_limit > 0
|
114
|
+
raise "Overlength" if @lbp_binary_limit > MaxBinaryLength # arbitrary sanity check
|
115
|
+
@lbp_binary_buffer = "\0" * @lbp_binary_limit
|
116
|
+
@lbp_binary_bytes_received = 0
|
117
|
+
end
|
118
|
+
|
119
|
+
@lbp_mode = :binary
|
120
|
+
if @lbp_data.length > 0
|
121
|
+
d,@lbp_data = @lbp_data,""
|
122
|
+
receive_data d
|
123
|
+
end
|
124
|
+
end
|
125
|
+
else
|
126
|
+
raise "invalid operation"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
#--
|
131
|
+
# For internal use, establish protocol baseline for handling lines.
|
132
|
+
def lbp_init_line_state
|
133
|
+
@lbp_data = ""
|
134
|
+
@lbp_mode = :lines
|
135
|
+
end
|
136
|
+
private :lbp_init_line_state
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|