m2r 2.0.2 → 2.1.0.pre
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.travis.yml +11 -3
- data/Vagrantfile +16 -0
- data/example/http_0mq.rb +4 -0
- data/kitchen/Rakefile +28 -0
- data/kitchen/auth.cfg +2 -0
- data/kitchen/cookbooks/build-essential/README.md +24 -0
- data/kitchen/cookbooks/build-essential/metadata.json +35 -0
- data/kitchen/cookbooks/build-essential/metadata.rb +10 -0
- data/kitchen/cookbooks/build-essential/recipes/default.rb +45 -0
- data/kitchen/cookbooks/essential/CHANGELOG.md +12 -0
- data/kitchen/cookbooks/essential/README.md +12 -0
- data/kitchen/cookbooks/essential/metadata.json +29 -0
- data/kitchen/cookbooks/essential/metadata.rb +6 -0
- data/kitchen/cookbooks/essential/recipes/default.rb +16 -0
- data/kitchen/cookbooks/m2r/CHANGELOG.md +12 -0
- data/kitchen/cookbooks/m2r/README.md +12 -0
- data/kitchen/cookbooks/m2r/metadata.json +29 -0
- data/kitchen/cookbooks/m2r/metadata.rb +6 -0
- data/kitchen/cookbooks/m2r/recipes/default.rb +13 -0
- data/kitchen/cookbooks/mongrel2/CHANGELOG.md +12 -0
- data/kitchen/cookbooks/mongrel2/README.md +12 -0
- data/kitchen/cookbooks/mongrel2/metadata.json +29 -0
- data/kitchen/cookbooks/mongrel2/metadata.rb +6 -0
- data/kitchen/cookbooks/mongrel2/recipes/default.rb +38 -0
- data/kitchen/cookbooks/ruby-build/README.md +12 -0
- data/kitchen/cookbooks/ruby-build/definitions/ruby.rb +65 -0
- data/kitchen/cookbooks/ruby-build/metadata.json +29 -0
- data/kitchen/cookbooks/ruby-build/metadata.rb +6 -0
- data/kitchen/cookbooks/ruby-build/recipes/default.rb +9 -0
- data/kitchen/cookbooks/zmq/CHANGELOG.md +12 -0
- data/kitchen/cookbooks/zmq/README.md +12 -0
- data/kitchen/cookbooks/zmq/metadata.json +29 -0
- data/kitchen/cookbooks/zmq/metadata.rb +6 -0
- data/kitchen/cookbooks/zmq/recipes/default.rb +36 -0
- data/kitchen/data_bags/README +1 -0
- data/kitchen/data_bags/vagrant.key +27 -0
- data/kitchen/data_bags/vagrant.pub +1 -0
- data/kitchen/m2r.cfg +5 -0
- data/kitchen/nodes/m2r.local.json +16 -0
- data/kitchen/roles/.gitkeep +0 -0
- data/kitchen/site-cookbooks/README +1 -0
- data/lib/m2r.rb +2 -0
- data/lib/m2r/connection.rb +30 -5
- data/lib/m2r/handler.rb +8 -0
- data/lib/m2r/multithread_handler.rb +27 -0
- data/lib/m2r/parser.rb +44 -0
- data/lib/m2r/rack_handler.rb +0 -2
- data/lib/m2r/request.rb +2 -21
- data/lib/m2r/version.rb +1 -1
- data/lib/rack/handler/mongrel2.rb +7 -3
- data/m2r.gemspec +3 -3
- data/test/support/test_handler.rb +29 -12
- data/test/test_helper.rb +1 -1
- data/test/unit/connection_test.rb +51 -16
- data/test/unit/handler_test.rb +33 -5
- data/test/unit/m2r_test.rb +2 -0
- data/test/unit/multithread_handler_test.rb +75 -0
- data/test/unit/rack_handler_test.rb +6 -2
- data/test/unit/{request_test.rb → request_parsing_test.rb} +5 -5
- metadata +71 -62
@@ -0,0 +1,38 @@
|
|
1
|
+
#
|
2
|
+
# Cookbook Name:: mongrel2
|
3
|
+
# Recipe:: default
|
4
|
+
#
|
5
|
+
# Copyright 2012, YOUR_COMPANY_NAME
|
6
|
+
#
|
7
|
+
# All rights reserved - Do Not Redistribute
|
8
|
+
#
|
9
|
+
|
10
|
+
%w[uuid-dev uuid-runtime libsqlite3-dev sqlite3].each do |name|
|
11
|
+
package name do
|
12
|
+
action :install
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
source = "https://github.com/zedshaw/mongrel2/tarball/v1.8.0"
|
17
|
+
name = "mongrel2-v1.8.0.tar.gz"
|
18
|
+
unpack = "zedshaw-mongrel2-bc721eb"
|
19
|
+
|
20
|
+
cache_dir = Chef::Config[:file_cache_path]
|
21
|
+
download_destination = File.join(cache_dir, name)
|
22
|
+
unpack_destination = File.join(cache_dir, unpack)
|
23
|
+
|
24
|
+
remote_file download_destination do
|
25
|
+
source source
|
26
|
+
mode "0644"
|
27
|
+
action :create_if_missing
|
28
|
+
end
|
29
|
+
|
30
|
+
execute "Extract mongrel2 archive" do
|
31
|
+
command "tar xvzf #{download_destination} -C #{cache_dir}"
|
32
|
+
creates unpack_destination
|
33
|
+
end
|
34
|
+
|
35
|
+
execute "Install mongrel2" do
|
36
|
+
command "cd #{unpack_destination} && make clean all && sudo make install"
|
37
|
+
not_if { `which m2sh | wc -l`.to_i > 0}
|
38
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
define :ruby do
|
2
|
+
version = params[:version]
|
3
|
+
home_dir = params[:home]
|
4
|
+
ruby_dir = "#{home_dir}/#{version}"
|
5
|
+
ruby_build_dir = "#{home_dir}/ruby-build"
|
6
|
+
rubygems = params[:rubygems]
|
7
|
+
owner = params[:owner]
|
8
|
+
bin_dir = "#{ruby_dir}/bin"
|
9
|
+
ruby_bin = "#{bin_dir}/ruby"
|
10
|
+
gem_bin = "#{bin_dir}/gem"
|
11
|
+
|
12
|
+
if params[:exports]
|
13
|
+
hash = params[:exports].inject(node.set){|memo, step| memo[step] }
|
14
|
+
hash['ruby_computed'] = {
|
15
|
+
'ruby_dir' => ruby_dir,
|
16
|
+
'bin_dir' => bin_dir,
|
17
|
+
'gem_bin' => gem_bin,
|
18
|
+
'ruby_bin' => ruby_bin,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
git ruby_build_dir do
|
23
|
+
repository "https://github.com/sstephenson/ruby-build.git"
|
24
|
+
reference "master"
|
25
|
+
action :sync
|
26
|
+
user owner
|
27
|
+
group owner
|
28
|
+
end
|
29
|
+
|
30
|
+
execute "install ruby #{ruby_dir}" do
|
31
|
+
command "#{ruby_build_dir}/bin/ruby-build #{version} #{ruby_dir}"
|
32
|
+
user owner
|
33
|
+
group owner
|
34
|
+
not_if { File.exists?(ruby_dir) }
|
35
|
+
end
|
36
|
+
|
37
|
+
profile_file = "#{home_dir}/.bashrc"
|
38
|
+
ruby_block "append ruby path #{ruby_dir}" do
|
39
|
+
path_definition = "export PATH=$HOME/#{version}/bin:$PATH"
|
40
|
+
block do
|
41
|
+
original_content = File.open(profile_file, 'r').read
|
42
|
+
File.open(profile_file, 'w') do |f|
|
43
|
+
f.puts "# Generated by chef"
|
44
|
+
f.puts path_definition
|
45
|
+
f.puts original_content
|
46
|
+
end
|
47
|
+
end
|
48
|
+
not_if { File.read(profile_file).include?(path_definition) }
|
49
|
+
end
|
50
|
+
|
51
|
+
if rubygems
|
52
|
+
execute "install rubygems - #{bin_dir}" do
|
53
|
+
user owner
|
54
|
+
cwd home_dir
|
55
|
+
command "#{bin_dir}/gem update --system #{rubygems}"
|
56
|
+
not_if %Q{test $(#{bin_dir}/gem --version) = "#{rubygems}"}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
execute "#{bin_dir}/gem install bundler --no-ri --no-rdoc" do
|
61
|
+
user owner
|
62
|
+
not_if "#{bin_dir}/gem list | grep -q bundler"
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
{
|
2
|
+
"name": "ruby-build",
|
3
|
+
"description": "Installs/Configures ruby-build",
|
4
|
+
"long_description": "Description\n===========\n\nRequirements\n============\n\nAttributes\n==========\n\nUsage\n=====\n\n",
|
5
|
+
"maintainer": "Arkency",
|
6
|
+
"maintainer_email": "michal.lomnicki@gmail.com",
|
7
|
+
"license": "All rights reserved",
|
8
|
+
"platforms": {
|
9
|
+
},
|
10
|
+
"dependencies": {
|
11
|
+
},
|
12
|
+
"recommendations": {
|
13
|
+
},
|
14
|
+
"suggestions": {
|
15
|
+
},
|
16
|
+
"conflicting": {
|
17
|
+
},
|
18
|
+
"providing": {
|
19
|
+
},
|
20
|
+
"replacing": {
|
21
|
+
},
|
22
|
+
"attributes": {
|
23
|
+
},
|
24
|
+
"groupings": {
|
25
|
+
},
|
26
|
+
"recipes": {
|
27
|
+
},
|
28
|
+
"version": "0.0.1"
|
29
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# CHANGELOG for zmq
|
2
|
+
|
3
|
+
This file is used to list changes made in each version of zmq.
|
4
|
+
|
5
|
+
## 0.1.0:
|
6
|
+
|
7
|
+
* Initial release of zmq
|
8
|
+
|
9
|
+
- - -
|
10
|
+
Check the [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) for help with Markdown.
|
11
|
+
|
12
|
+
The [Github Flavored Markdown page](http://github.github.com/github-flavored-markdown/) describes the differences between markdown on github and standard markdown.
|
@@ -0,0 +1,29 @@
|
|
1
|
+
{
|
2
|
+
"name": "zmq",
|
3
|
+
"description": "Installs/Configures zmq",
|
4
|
+
"long_description": "Description\n===========\n\nRequirements\n============\n\nAttributes\n==========\n\nUsage\n=====\n\n",
|
5
|
+
"maintainer": "YOUR_COMPANY_NAME",
|
6
|
+
"maintainer_email": "YOUR_EMAIL",
|
7
|
+
"license": "All rights reserved",
|
8
|
+
"platforms": {
|
9
|
+
},
|
10
|
+
"dependencies": {
|
11
|
+
},
|
12
|
+
"recommendations": {
|
13
|
+
},
|
14
|
+
"suggestions": {
|
15
|
+
},
|
16
|
+
"conflicting": {
|
17
|
+
},
|
18
|
+
"providing": {
|
19
|
+
},
|
20
|
+
"replacing": {
|
21
|
+
},
|
22
|
+
"attributes": {
|
23
|
+
},
|
24
|
+
"groupings": {
|
25
|
+
},
|
26
|
+
"recipes": {
|
27
|
+
},
|
28
|
+
"version": "0.1.0"
|
29
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Cookbook Name:: zmq
|
3
|
+
# Recipe:: default
|
4
|
+
#
|
5
|
+
# Copyright 2012, Arkency
|
6
|
+
#
|
7
|
+
# All rights reserved - Do Not Redistribute
|
8
|
+
#
|
9
|
+
|
10
|
+
package 'uuid-dev'
|
11
|
+
|
12
|
+
zmq = node['zmq'] || {}
|
13
|
+
zmq_v = zmq['version'] || '2.2.0'
|
14
|
+
#zmq_v = zmq['version'] || '3.2.1-rc2'
|
15
|
+
name = "zeromq-#{zmq_v}.tar.gz"
|
16
|
+
unpack = 'zeromq-' + zmq_v.split("-").first
|
17
|
+
|
18
|
+
cache_dir = Chef::Config[:file_cache_path]
|
19
|
+
download_destination = File.join(cache_dir, name)
|
20
|
+
unpack_destination = File.join(cache_dir, unpack)
|
21
|
+
|
22
|
+
remote_file download_destination do
|
23
|
+
source "http://download.zeromq.org/#{name}"
|
24
|
+
mode "0644"
|
25
|
+
action :create_if_missing
|
26
|
+
end
|
27
|
+
|
28
|
+
execute "Extract zmq #{zmq_v} archive" do
|
29
|
+
command "tar xvzf #{download_destination} -C #{cache_dir}"
|
30
|
+
creates unpack_destination
|
31
|
+
end
|
32
|
+
|
33
|
+
execute "Install zmq #{zmq_v} version" do
|
34
|
+
command "cd #{unpack_destination} && ./configure && make && sudo make install && sudo ldconfig"
|
35
|
+
not_if { `ldconfig -p | grep libzmq | wc -l`.to_i > 0}
|
36
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI
|
3
|
+
w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP
|
4
|
+
kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2
|
5
|
+
hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO
|
6
|
+
Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW
|
7
|
+
yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd
|
8
|
+
ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1
|
9
|
+
Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf
|
10
|
+
TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK
|
11
|
+
iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A
|
12
|
+
sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf
|
13
|
+
4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP
|
14
|
+
cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk
|
15
|
+
EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN
|
16
|
+
CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX
|
17
|
+
3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG
|
18
|
+
YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj
|
19
|
+
3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+
|
20
|
+
dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz
|
21
|
+
6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC
|
22
|
+
P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF
|
23
|
+
llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ
|
24
|
+
kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH
|
25
|
+
+vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ
|
26
|
+
NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s=
|
27
|
+
-----END RSA PRIVATE KEY-----
|
@@ -0,0 +1 @@
|
|
1
|
+
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key
|
data/kitchen/m2r.cfg
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"name": "m2r.local",
|
3
|
+
"env": "development",
|
4
|
+
"ipaddress": "172.26.66.100",
|
5
|
+
"run_list": [
|
6
|
+
"recipe[essential]",
|
7
|
+
"recipe[build-essential]",
|
8
|
+
"recipe[ruby-build]",
|
9
|
+
"recipe[zmq]",
|
10
|
+
"recipe[mongrel2]",
|
11
|
+
"recipe[m2r]"
|
12
|
+
],
|
13
|
+
"ruby": "1.9.3-p286",
|
14
|
+
"username":"vagrant",
|
15
|
+
"home_root":"/home"
|
16
|
+
}
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
|
data/lib/m2r.rb
CHANGED
data/lib/m2r/connection.rb
CHANGED
@@ -3,7 +3,12 @@ require 'm2r'
|
|
3
3
|
module M2R
|
4
4
|
# Connection for exchanging data with mongrel2
|
5
5
|
class Connection
|
6
|
-
class Error < StandardError
|
6
|
+
class Error < StandardError
|
7
|
+
attr_accessor :errno
|
8
|
+
def signal?
|
9
|
+
errno == ZMQ::EINTR
|
10
|
+
end
|
11
|
+
end
|
7
12
|
|
8
13
|
# @param [ZMQ::Socket] request_socket socket for receiving requests
|
9
14
|
# from Mongrel2
|
@@ -30,7 +35,11 @@ module M2R
|
|
30
35
|
# @api public
|
31
36
|
def receive
|
32
37
|
ret = @request_socket.recv_string(msg = "")
|
33
|
-
|
38
|
+
if ret < 0
|
39
|
+
e = Error.new "Unable to receive message: #{ZMQ::Util.error_string}"
|
40
|
+
e.errno = ZMQ::Util.errno
|
41
|
+
raise e
|
42
|
+
end
|
34
43
|
return msg
|
35
44
|
end
|
36
45
|
|
@@ -54,11 +63,27 @@ module M2R
|
|
54
63
|
# @return [String] M2 response message
|
55
64
|
#
|
56
65
|
# @api public
|
57
|
-
def deliver(uuid, connection_ids, data)
|
66
|
+
def deliver(uuid, connection_ids, data, trial = 1)
|
58
67
|
msg = "#{uuid} #{TNetstring.dump([*connection_ids].join(' '))} #{data}"
|
59
|
-
ret = @response_socket.send_string(msg, ZMQ::
|
60
|
-
|
68
|
+
ret = @response_socket.send_string(msg, ZMQ::NonBlocking)
|
69
|
+
if ret < 0
|
70
|
+
e = Error.new "Unable to deliver message: #{ZMQ::Util.error_string}"
|
71
|
+
e.errno = ZMQ::Util.errno
|
72
|
+
raise e
|
73
|
+
end
|
61
74
|
return msg
|
75
|
+
rescue Connection::Error => er
|
76
|
+
raise if trial >= 3
|
77
|
+
raise unless er.signal?
|
78
|
+
deliver(uuid, connection_ids, data, trial + 1)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Closes ZMQ sockets
|
82
|
+
#
|
83
|
+
# @api public
|
84
|
+
def close
|
85
|
+
@request_socket.close
|
86
|
+
@response_socket.close
|
62
87
|
end
|
63
88
|
|
64
89
|
private
|
data/lib/m2r/handler.rb
CHANGED
@@ -30,6 +30,7 @@ module M2R
|
|
30
30
|
catch(:stop) do
|
31
31
|
loop { one_loop }
|
32
32
|
end
|
33
|
+
@connection.close
|
33
34
|
end
|
34
35
|
|
35
36
|
# Schedule stop after processing request
|
@@ -127,6 +128,12 @@ module M2R
|
|
127
128
|
def on_error(request, response, error)
|
128
129
|
end
|
129
130
|
|
131
|
+
# Callback when ZMQ interrupted by signal
|
132
|
+
# @api public
|
133
|
+
# @!visibility public
|
134
|
+
def on_interrupted
|
135
|
+
end
|
136
|
+
|
130
137
|
private
|
131
138
|
|
132
139
|
def next_request
|
@@ -138,6 +145,7 @@ module M2R
|
|
138
145
|
throw :stop if stop?
|
139
146
|
response = request_lifecycle(request = next_request)
|
140
147
|
rescue => error
|
148
|
+
return on_interrupted if Connection::Error === error && error.signal?
|
141
149
|
on_error(request, response, error)
|
142
150
|
end
|
143
151
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module M2R
|
2
|
+
class MultithreadHandler
|
3
|
+
|
4
|
+
attr_reader :threads
|
5
|
+
|
6
|
+
def initialize(singlethread_handler_factory)
|
7
|
+
@singlethread_handler_factory = singlethread_handler_factory
|
8
|
+
end
|
9
|
+
|
10
|
+
def listen
|
11
|
+
@threads = 8.times.map do
|
12
|
+
Thread.new do
|
13
|
+
handler = @singlethread_handler_factory.new
|
14
|
+
Thread.current[:m2r_handler] = handler
|
15
|
+
handler.listen
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop
|
21
|
+
@threads.each do |t|
|
22
|
+
t[:m2r_handler].stop
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|