thin 0.6.4-x86-mswin32-60 → 0.7.0-x86-mswin32-60
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of thin might be problematic. Click here for more details.
- data/CHANGELOG +20 -0
- data/README +11 -12
- data/benchmark/abc +51 -0
- data/benchmark/benchmarker.rb +80 -0
- data/benchmark/runner +79 -0
- data/example/adapter.rb +3 -3
- data/example/thin.god +11 -7
- data/lib/thin.rb +17 -16
- data/lib/thin/command.rb +10 -4
- data/lib/thin/connection.rb +51 -9
- data/lib/thin/connectors/connector.rb +22 -10
- data/lib/thin/connectors/swiftiply_client.rb +55 -0
- data/lib/thin/connectors/unix_server.rb +5 -7
- data/lib/thin/controllers/cluster.rb +28 -22
- data/lib/thin/controllers/controller.rb +74 -14
- data/lib/thin/controllers/service.rb +1 -1
- data/lib/thin/daemonizing.rb +6 -4
- data/lib/thin/headers.rb +4 -0
- data/lib/thin/logging.rb +34 -9
- data/lib/thin/request.rb +31 -2
- data/lib/thin/response.rb +22 -7
- data/lib/thin/runner.rb +27 -14
- data/lib/thin/server.rb +55 -7
- data/lib/thin/version.rb +3 -3
- data/lib/thin_parser.so +0 -0
- data/spec/command_spec.rb +2 -3
- data/spec/connection_spec.rb +25 -1
- data/spec/connectors/swiftiply_client_spec.rb +66 -0
- data/spec/controllers/cluster_spec.rb +43 -12
- data/spec/controllers/controller_spec.rb +16 -4
- data/spec/controllers/service_spec.rb +0 -1
- data/spec/logging_spec.rb +42 -0
- data/spec/request/persistent_spec.rb +35 -0
- data/spec/response_spec.rb +18 -0
- data/spec/server/pipelining_spec.rb +108 -0
- data/spec/server/swiftiply.yml +6 -0
- data/spec/server/swiftiply_spec.rb +27 -0
- data/spec/server/tcp_spec.rb +3 -3
- data/spec/server_spec.rb +22 -0
- data/spec/spec_helper.rb +3 -3
- data/tasks/gem.rake +1 -1
- data/tasks/spec.rake +9 -0
- metadata +14 -6
- data/benchmark/previous.rb +0 -14
- data/benchmark/simple.rb +0 -15
- data/benchmark/utils.rb +0 -75
data/lib/thin/request.rb
CHANGED
@@ -13,11 +13,16 @@ module Thin
|
|
13
13
|
MAX_BODY = 1024 * (80 + 32)
|
14
14
|
BODY_TMPFILE = 'thin-body'.freeze
|
15
15
|
|
16
|
-
# Freeze some HTTP header names
|
16
|
+
# Freeze some HTTP header names & values
|
17
17
|
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
18
|
+
HTTP_VERSION = 'HTTP_VERSION'.freeze
|
19
|
+
HTTP_1_0 = 'HTTP/1.0'.freeze
|
18
20
|
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
|
19
21
|
FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'.freeze
|
20
22
|
CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
|
23
|
+
CONNECTION = 'HTTP_CONNECTION'.freeze
|
24
|
+
KEEP_ALIVE_REGEXP = /keep-alive/i
|
25
|
+
NOT_CLOSE_REGEXP = /[^(close)]/i
|
21
26
|
|
22
27
|
# Freeze some Rack header names
|
23
28
|
RACK_INPUT = 'rack.input'.freeze
|
@@ -90,7 +95,31 @@ module Thin
|
|
90
95
|
@env[CONTENT_LENGTH].to_i
|
91
96
|
end
|
92
97
|
|
93
|
-
#
|
98
|
+
# Returns +true+ if the client expect the connection to be persistent.
|
99
|
+
def persistent?
|
100
|
+
# Clients and servers SHOULD NOT assume that a persistent connection
|
101
|
+
# is maintained for HTTP versions less than 1.1 unless it is explicitly
|
102
|
+
# signaled. (http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html)
|
103
|
+
if @env[HTTP_VERSION] == HTTP_1_0
|
104
|
+
@env[CONNECTION] =~ KEEP_ALIVE_REGEXP
|
105
|
+
|
106
|
+
# HTTP/1.1 client intends to maintain a persistent connection unless
|
107
|
+
# a Connection header including the connection-token "close" was sent
|
108
|
+
# in the request
|
109
|
+
else
|
110
|
+
@env[CONNECTION].nil? || @env[CONNECTION] =~ NOT_CLOSE_REGEXP
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def remote_address=(address)
|
115
|
+
@env[REMOTE_ADDR] = address
|
116
|
+
end
|
117
|
+
|
118
|
+
def forwarded_for
|
119
|
+
@env[FORWARDED_FOR]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Close any resource used by the request
|
94
123
|
def close
|
95
124
|
@body.delete if @body.class == Tempfile
|
96
125
|
end
|
data/lib/thin/response.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
module Thin
|
2
2
|
# A response sent to the client.
|
3
3
|
class Response
|
4
|
-
CONNECTION
|
5
|
-
|
6
|
-
|
4
|
+
CONNECTION = 'Connection'.freeze
|
5
|
+
CLOSE = 'close'.freeze
|
6
|
+
KEEP_ALIVE = 'keep-alive'.freeze
|
7
|
+
SERVER = 'Server'.freeze
|
8
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
7
9
|
|
8
10
|
# Status code
|
9
11
|
attr_accessor :status
|
@@ -15,19 +17,21 @@ module Thin
|
|
15
17
|
attr_reader :headers
|
16
18
|
|
17
19
|
def initialize
|
18
|
-
@headers
|
19
|
-
@status
|
20
|
+
@headers = Headers.new
|
21
|
+
@status = 200
|
22
|
+
@persistent = false
|
20
23
|
end
|
21
24
|
|
22
25
|
# String representation of the headers
|
23
26
|
# to be sent in the response.
|
24
27
|
def headers_output
|
25
|
-
|
28
|
+
# Set default headers
|
29
|
+
@headers[CONNECTION] = persistent? ? KEEP_ALIVE : CLOSE
|
26
30
|
@headers[SERVER] = Thin::SERVER
|
27
31
|
|
28
32
|
@headers.to_s
|
29
33
|
end
|
30
|
-
|
34
|
+
|
31
35
|
# Top header of the response,
|
32
36
|
# containing the status code and response headers.
|
33
37
|
def head
|
@@ -77,5 +81,16 @@ module Thin
|
|
77
81
|
yield chunk
|
78
82
|
end
|
79
83
|
end
|
84
|
+
|
85
|
+
# Tell the client the connection should stay open
|
86
|
+
def persistent!
|
87
|
+
@persistent = true
|
88
|
+
end
|
89
|
+
|
90
|
+
# Persistent connection must be requested as keep-alive
|
91
|
+
# from the server and have a Content-Length.
|
92
|
+
def persistent?
|
93
|
+
@persistent && @headers.has_key?(CONTENT_LENGTH)
|
94
|
+
end
|
80
95
|
end
|
81
96
|
end
|
data/lib/thin/runner.rb
CHANGED
@@ -32,13 +32,15 @@ module Thin
|
|
32
32
|
|
33
33
|
# Default options values
|
34
34
|
@options = {
|
35
|
-
:chdir
|
36
|
-
:environment
|
37
|
-
:address
|
38
|
-
:port
|
39
|
-
:timeout
|
40
|
-
:log
|
41
|
-
:pid
|
35
|
+
:chdir => Dir.pwd,
|
36
|
+
:environment => 'development',
|
37
|
+
:address => '0.0.0.0',
|
38
|
+
:port => Server::DEFAULT_PORT,
|
39
|
+
:timeout => Server::DEFAULT_TIMEOUT,
|
40
|
+
:log => 'log/thin.log',
|
41
|
+
:pid => 'tmp/pids/thin.pid',
|
42
|
+
:max_conns => Server::DEFAULT_MAXIMUM_CONNECTIONS,
|
43
|
+
:max_persistent_conns => Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
|
42
44
|
}
|
43
45
|
|
44
46
|
parse!
|
@@ -59,13 +61,12 @@ module Thin
|
|
59
61
|
"(default: #{@options[:address]})") { |host| @options[:address] = host }
|
60
62
|
opts.on("-p", "--port PORT", "use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
|
61
63
|
opts.on("-S", "--socket FILE", "bind to unix domain socket") { |file| @options[:socket] = file }
|
64
|
+
opts.on("-y", "--swiftiply [KEY]", "Run using swiftiply") { |key| @options[:swiftiply] = key }
|
62
65
|
opts.on("-e", "--environment ENV", "Rails environment " +
|
63
66
|
"(default: #{@options[:environment]})") { |env| @options[:environment] = env }
|
64
67
|
opts.on("-c", "--chdir DIR", "Change to dir before starting") { |dir| @options[:chdir] = File.expand_path(dir) }
|
65
|
-
opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
|
66
|
-
"(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
|
67
68
|
opts.on("-r", "--rackup FILE", "Load a Rack config file instead of " +
|
68
|
-
"
|
69
|
+
"Rails adapter") { |file| @options[:rackup] = file }
|
69
70
|
opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| @options[:prefix] = path }
|
70
71
|
opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| @options[:stats] = path }
|
71
72
|
|
@@ -90,13 +91,25 @@ module Thin
|
|
90
91
|
opts.on( "--all [DIR]", "Send command to each config files in DIR") { |dir| @options[:all] = dir } if Thin.linux?
|
91
92
|
end
|
92
93
|
|
94
|
+
opts.separator ""
|
95
|
+
opts.separator "Tuning options:"
|
96
|
+
|
97
|
+
opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
|
98
|
+
"(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
|
99
|
+
opts.on( "--max-conns NUM", "Maximum number of connections " +
|
100
|
+
"(default: #{@options[:max_conns]})",
|
101
|
+
"Might require sudo to set higher then 1024") { |num| @options[:max_conns] = num.to_i } unless Thin.win?
|
102
|
+
opts.on( "--max-persistent-conns NUM",
|
103
|
+
"Maximum number of persistent connections",
|
104
|
+
"(default: #{@options[:max_persistent_conns]})") { |num| @options[:max_persistent_conns] = num.to_i }
|
105
|
+
|
93
106
|
opts.separator ""
|
94
107
|
opts.separator "Common options:"
|
95
108
|
|
96
|
-
opts.on_tail("-D", "--debug", "Set debbuging on")
|
97
|
-
opts.on_tail("-V", "--trace", "Set tracing on")
|
98
|
-
opts.on_tail("-h", "--help", "Show this message")
|
99
|
-
opts.on_tail('-v', '--version', "Show version")
|
109
|
+
opts.on_tail("-D", "--debug", "Set debbuging on") { Logging.debug = true }
|
110
|
+
opts.on_tail("-V", "--trace", "Set tracing on (log raw request/response)") { Logging.trace = true }
|
111
|
+
opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
|
112
|
+
opts.on_tail('-v', '--version', "Show version") { puts Thin::SERVER; exit }
|
100
113
|
end
|
101
114
|
end
|
102
115
|
|
data/lib/thin/server.rb
CHANGED
@@ -45,6 +45,12 @@ module Thin
|
|
45
45
|
include Logging
|
46
46
|
include Daemonizable
|
47
47
|
extend Forwardable
|
48
|
+
|
49
|
+
# Default values
|
50
|
+
DEFAULT_TIMEOUT = 30 #sec
|
51
|
+
DEFAULT_PORT = 3000
|
52
|
+
DEFAULT_MAXIMUM_CONNECTIONS = 1024
|
53
|
+
DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS = 512
|
48
54
|
|
49
55
|
# Application (Rack adapter) called with the request that produces the response.
|
50
56
|
attr_accessor :app
|
@@ -52,17 +58,26 @@ module Thin
|
|
52
58
|
# Connector handling the connections to the clients.
|
53
59
|
attr_accessor :connector
|
54
60
|
|
61
|
+
# Maximum number of file or socket descriptors that the server may open.
|
62
|
+
attr_accessor :maximum_connections
|
63
|
+
|
55
64
|
# Maximum number of seconds for incoming data to arrive before the connection
|
56
65
|
# is dropped.
|
57
66
|
def_delegators :@connector, :timeout, :timeout=
|
58
67
|
|
68
|
+
# Maximum number of connection that can be persistent at the same time.
|
69
|
+
# Most browser never close the connection so most of the time they are closed
|
70
|
+
# when the timeout occur. If we don't control the number of persistent connection,
|
71
|
+
# if would be very easy to overflow the server for a DoS attack.
|
72
|
+
def_delegators :@connector, :maximum_persistent_connections, :maximum_persistent_connections=
|
73
|
+
|
59
74
|
# Address and port on which the server is listening for connections.
|
60
75
|
def_delegators :@connector, :host, :port
|
61
76
|
|
62
77
|
# UNIX domain socket on which the server is listening for connections.
|
63
78
|
def_delegator :@connector, :socket
|
64
79
|
|
65
|
-
def initialize(host_or_socket_or_connector, port=
|
80
|
+
def initialize(host_or_socket_or_connector, port=DEFAULT_PORT, app=nil, &block)
|
66
81
|
# Try to intelligently select which connector to use.
|
67
82
|
@connector = case
|
68
83
|
when host_or_socket_or_connector.is_a?(Connectors::Connector)
|
@@ -73,11 +88,19 @@ module Thin
|
|
73
88
|
Connectors::TcpServer.new(host_or_socket_or_connector, port.to_i)
|
74
89
|
end
|
75
90
|
|
76
|
-
@connector.server = self
|
77
91
|
@app = app
|
92
|
+
@connector.server = self
|
93
|
+
|
94
|
+
# Set defaults
|
95
|
+
@maximum_connections = DEFAULT_MAXIMUM_CONNECTIONS
|
96
|
+
@connector.maximum_persistent_connections = DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
|
97
|
+
@connector.timeout = DEFAULT_TIMEOUT
|
78
98
|
|
79
99
|
# Allow using Rack builder as a block
|
80
100
|
@app = Rack::Builder.new(&block).to_app if block
|
101
|
+
|
102
|
+
# If in debug mode, wrap in logger adapter
|
103
|
+
@app = Rack::CommonLogger.new(@app) if Logging.debug?
|
81
104
|
end
|
82
105
|
|
83
106
|
# Lil' shortcut to turn this:
|
@@ -91,7 +114,7 @@ module Thin
|
|
91
114
|
def self.start(*args, &block)
|
92
115
|
new(*args, &block).start!
|
93
116
|
end
|
94
|
-
|
117
|
+
|
95
118
|
# Start the server and listen for connections.
|
96
119
|
# Also register signals:
|
97
120
|
# * INT calls +stop+ to shutdown gracefully.
|
@@ -99,16 +122,17 @@ module Thin
|
|
99
122
|
def start
|
100
123
|
raise ArgumentError, 'app required' unless @app
|
101
124
|
|
102
|
-
|
103
|
-
trap('TERM') { stop! }
|
125
|
+
setup_signals
|
104
126
|
|
105
127
|
# See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
|
106
128
|
EventMachine.epoll
|
107
129
|
|
108
130
|
log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::CODENAME})"
|
131
|
+
debug ">> Debugging ON"
|
109
132
|
trace ">> Tracing ON"
|
110
|
-
|
133
|
+
|
111
134
|
log ">> Listening on #{@connector}, CTRL+C to stop"
|
135
|
+
|
112
136
|
@running = true
|
113
137
|
EventMachine.run { @connector.connect }
|
114
138
|
end
|
@@ -160,15 +184,39 @@ module Thin
|
|
160
184
|
@running
|
161
185
|
end
|
162
186
|
|
187
|
+
# Set the maximum number of socket descriptors that the server may open.
|
188
|
+
# The process needs to have required privilege to set it higher the 1024 on
|
189
|
+
# some systems.
|
190
|
+
def set_descriptor_table_size!
|
191
|
+
return 0 if Thin.win? # Not supported on Windows
|
192
|
+
|
193
|
+
requested_maximum_connections = @maximum_connections
|
194
|
+
@maximum_connections = EventMachine.set_descriptor_table_size(requested_maximum_connections)
|
195
|
+
|
196
|
+
log ">> Setting maximum connections to #{@maximum_connections}"
|
197
|
+
if @maximum_connections < requested_maximum_connections
|
198
|
+
log "!! Maximum connections smaller then requested, " +
|
199
|
+
"run with sudo to set higher"
|
200
|
+
end
|
201
|
+
|
202
|
+
@maximum_connections
|
203
|
+
end
|
204
|
+
|
163
205
|
protected
|
164
206
|
def wait_for_connections_and_stop
|
165
207
|
if @connector.empty?
|
166
208
|
stop!
|
167
209
|
true
|
168
210
|
else
|
169
|
-
log ">> Waiting for #{@connector.size} connection(s) to finish, CTRL+C to
|
211
|
+
log ">> Waiting for #{@connector.size} connection(s) to finish, can take up to #{timeout} sec, CTRL+C to stop now"
|
170
212
|
false
|
171
213
|
end
|
172
214
|
end
|
215
|
+
|
216
|
+
def setup_signals
|
217
|
+
trap('QUIT') { stop } unless Thin.win?
|
218
|
+
trap('INT') { stop! }
|
219
|
+
trap('TERM') { stop! }
|
220
|
+
end
|
173
221
|
end
|
174
222
|
end
|
data/lib/thin/version.rb
CHANGED
data/lib/thin_parser.so
CHANGED
Binary file
|
data/spec/command_spec.rb
CHANGED
@@ -2,13 +2,12 @@ require File.dirname(__FILE__) + '/spec_helper'
|
|
2
2
|
|
3
3
|
describe Command do
|
4
4
|
before do
|
5
|
-
@command = Command.new(:start, :port => 3000, :daemonize => true, :log => 'hi.log'
|
6
|
-
@command.silent = true
|
5
|
+
@command = Command.new(:start, :port => 3000, :daemonize => true, :log => 'hi.log')
|
7
6
|
end
|
8
7
|
|
9
8
|
it 'should shellify command' do
|
10
9
|
out = @command.shellify
|
11
10
|
out.should include('--port=3000', '--daemonize', '--log="hi.log"', 'thin start --')
|
12
|
-
out.should_not include('--pid
|
11
|
+
out.should_not include('--pid')
|
13
12
|
end
|
14
13
|
end
|
data/spec/connection_spec.rb
CHANGED
@@ -3,7 +3,6 @@ require File.dirname(__FILE__) + '/spec_helper'
|
|
3
3
|
describe Connection do
|
4
4
|
before do
|
5
5
|
@connection = Connection.new(mock('EM', :null_object => true))
|
6
|
-
@connection.silent = true
|
7
6
|
@connection.post_init
|
8
7
|
@connection.app = proc do |env|
|
9
8
|
[200, {}, ['']]
|
@@ -41,8 +40,33 @@ describe Connection do
|
|
41
40
|
@connection.remote_address.should be_nil
|
42
41
|
end
|
43
42
|
|
43
|
+
it "should return nil on nil get_peername" do
|
44
|
+
@connection.stub!(:get_peername).and_return(nil)
|
45
|
+
@connection.remote_address.should be_nil
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should return nil on empty get_peername" do
|
49
|
+
@connection.stub!(:get_peername).and_return('')
|
50
|
+
@connection.remote_address.should be_nil
|
51
|
+
end
|
52
|
+
|
44
53
|
it "should return remote_address" do
|
45
54
|
@connection.stub!(:get_peername).and_return("\020\002?E\177\000\000\001\000\000\000\000\000\000\000\000")
|
46
55
|
@connection.remote_address.should == '127.0.0.1'
|
47
56
|
end
|
57
|
+
|
58
|
+
it "should not be persistent" do
|
59
|
+
@connection.should_not be_persistent
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should be persistent when response is and allowed" do
|
63
|
+
@connection.response.stub!(:persistent?).and_return(true)
|
64
|
+
@connection.can_persist!
|
65
|
+
@connection.should be_persistent
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should not be persistent when response is but not allowed" do
|
69
|
+
@connection.response.persistent!
|
70
|
+
@connection.should_not be_persistent
|
71
|
+
end
|
48
72
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Connectors::SwiftiplyClient do
|
4
|
+
before do
|
5
|
+
@connector = Connectors::SwiftiplyClient.new('0.0.0.0', 3333)
|
6
|
+
@connector.server = mock('server', :null_object => true)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should connect" do
|
10
|
+
EventMachine.run do
|
11
|
+
@connector.connect
|
12
|
+
EventMachine.stop
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should disconnect" do
|
17
|
+
EventMachine.run do
|
18
|
+
@connector.connect
|
19
|
+
@connector.disconnect
|
20
|
+
EventMachine.stop
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe SwiftiplyConnection do
|
26
|
+
before do
|
27
|
+
@connection = SwiftiplyConnection.new(nil)
|
28
|
+
@connection.connector = Connectors::SwiftiplyClient.new('0.0.0.0', 3333)
|
29
|
+
@connection.connector.server = mock('server', :null_object => true)
|
30
|
+
end
|
31
|
+
|
32
|
+
it do
|
33
|
+
@connection.should be_persistent
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should send handshake on connection_completed" do
|
37
|
+
@connection.should_receive(:send_data).with('swiftclient000000000d0500')
|
38
|
+
@connection.connection_completed
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should reconnect on unbind" do
|
42
|
+
@connection.connector.stub!(:running?).and_return(true)
|
43
|
+
@connection.stub!(:rand).and_return(0) # Make sure we don't wait
|
44
|
+
|
45
|
+
@connection.should_receive(:reconnect).with('0.0.0.0', 3333)
|
46
|
+
|
47
|
+
EventMachine.run do
|
48
|
+
@connection.unbind
|
49
|
+
EventMachine.add_timer(0) { EventMachine.stop }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should not reconnect when not running" do
|
54
|
+
@connection.connector.stub!(:running?).and_return(false)
|
55
|
+
EventMachine.should_not_receive(:add_timer)
|
56
|
+
@connection.unbind
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should have a host_ip" do
|
60
|
+
@connection.send(:host_ip).should == [0, 0, 0, 0]
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should generate swiftiply_handshake based on key" do
|
64
|
+
@connection.send(:swiftiply_handshake, 'key').should == 'swiftclient000000000d0503key'
|
65
|
+
end
|
66
|
+
end
|