rexec 1.1.10
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +14 -0
- data/bin/daemon-exec +116 -0
- data/lib/rexec.rb +33 -0
- data/lib/rexec/client.rb +23 -0
- data/lib/rexec/connection.rb +153 -0
- data/lib/rexec/daemon.rb +27 -0
- data/lib/rexec/daemon/base.rb +151 -0
- data/lib/rexec/daemon/controller.rb +192 -0
- data/lib/rexec/daemon/pidfile.rb +64 -0
- data/lib/rexec/priviledges.rb +28 -0
- data/lib/rexec/reverse_io.rb +70 -0
- data/lib/rexec/server.rb +62 -0
- data/lib/rexec/task.rb +325 -0
- data/lib/rexec/version.rb +24 -0
- data/test/client.rb +31 -0
- data/test/daemon.rb +68 -0
- data/test/daemon_test.rb +44 -0
- data/test/listing_example.rb +49 -0
- data/test/remote_server_test.rb +63 -0
- data/test/server_test.rb +89 -0
- data/test/task.rb +24 -0
- data/test/task_test.rb +165 -0
- metadata +87 -0
data/README.rdoc
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
= RExec
|
2
|
+
|
3
|
+
Author:: Samuel Williams (http://www.oriontransfer.co.nz/)
|
4
|
+
Copyright:: Copyright (C) 2009, 2010 Samuel Williams
|
5
|
+
License:: GPLv3
|
6
|
+
|
7
|
+
RExec stands for Ruby Execute or Remote Execute (depending on how you use it). It provides a number of different things to assist with running Ruby code:
|
8
|
+
|
9
|
+
* A framework to send Ruby code to a remote server for execution
|
10
|
+
* A framework for writing command line daemons (i.e. <tt>start</tt>, <tt>restart</tt>, <tt>stop</tt>, <tt>status</tt>)
|
11
|
+
* A comprehensive <tt>Task</tt> class for launching tasks, managing input and output, exit status, etc
|
12
|
+
* Basic privilege management code for changing the processes owner
|
13
|
+
* A bunch of helpers for various different things (such as reading a file backwards)
|
14
|
+
* <tt>daemon-exec</tt> executable for running regular shell tasks in the background
|
data/bin/daemon-exec
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright (c) 2010 Samuel Williams. Released under the GNU GPLv3.
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require 'rubygems'
|
19
|
+
|
20
|
+
require 'rexec'
|
21
|
+
require 'optparse'
|
22
|
+
|
23
|
+
OPTIONS = {
|
24
|
+
:out => "/tmp/daemon-exec.log",
|
25
|
+
:err => "/tmp/daemon-exec-error.log",
|
26
|
+
:in => "/dev/null",
|
27
|
+
:print_pid => false,
|
28
|
+
:root => "/",
|
29
|
+
:verbose => false,
|
30
|
+
:relocate => true,
|
31
|
+
:read_stdin => false,
|
32
|
+
}
|
33
|
+
|
34
|
+
ARGV.options do |o|
|
35
|
+
script_name = File.basename($0)
|
36
|
+
|
37
|
+
o.set_summary_indent("\t")
|
38
|
+
o.banner = "Usage: #{script_name} [-I stdin] [-O stdout] [-E stderr] [script/stdin]"
|
39
|
+
o.define_head "Copyright (c) 2010 Samuel Williams <http://www.oriontransfer.co.nz/>."
|
40
|
+
|
41
|
+
o.on("-d [dir]", String, "Daemons working path, default /") do |dir|
|
42
|
+
OPTIONS[:root] = dir
|
43
|
+
end
|
44
|
+
|
45
|
+
o.on("-s", "Don't attempt to relocate arguments to absolute paths") do
|
46
|
+
OPTIONS[:relocate] = false
|
47
|
+
end
|
48
|
+
|
49
|
+
o.define "File / Pipe Options:"
|
50
|
+
|
51
|
+
o.on("-I [path]", String, "File for STDIN, defaults to #{OPTIONS[:in]}; Note: Use -I - to send data from current STDIN") do |path|
|
52
|
+
OPTIONS[:in] = path
|
53
|
+
end
|
54
|
+
|
55
|
+
o.on("-O [path]", String, "File for STDOUT, defaults to #{OPTIONS[:out]}") do |path|
|
56
|
+
OPTIONS[:in] = path
|
57
|
+
end
|
58
|
+
|
59
|
+
o.on("-E [path]", String, "File for STDERR, defaults to #{OPTIONS[:err]}") do |path|
|
60
|
+
OPTIONS[:err] = path
|
61
|
+
end
|
62
|
+
|
63
|
+
o.define "Misc Options:"
|
64
|
+
|
65
|
+
o.on("-p", "Print out the PID of the forked process") do
|
66
|
+
OPTIONS[:print_pid] = true
|
67
|
+
end
|
68
|
+
|
69
|
+
o.on("-V", "Print verbose information about what is going on") do
|
70
|
+
OPTIONS[:verbose] = true
|
71
|
+
end
|
72
|
+
|
73
|
+
o.on("-h", "Show this help/version information and exit") do
|
74
|
+
puts o
|
75
|
+
exit 0
|
76
|
+
end
|
77
|
+
end.parse!
|
78
|
+
|
79
|
+
if OPTIONS[:relocate]
|
80
|
+
ARGV.collect! do |value|
|
81
|
+
if File.exist?(value)
|
82
|
+
File.expand_path(value)
|
83
|
+
else
|
84
|
+
value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
[:in, :out, :err].each do |path|
|
89
|
+
OPTIONS[path] = File.expand_path(OPTIONS[path]) unless OPTIONS[path] == "-"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if OPTIONS[:verbose]
|
94
|
+
puts "Running #{ARGV.inspect}"
|
95
|
+
end
|
96
|
+
|
97
|
+
task_options = {
|
98
|
+
:daemonize => true,
|
99
|
+
:out => File.open(OPTIONS[:out], "a"),
|
100
|
+
:err => File.open(OPTIONS[:err], "a")
|
101
|
+
}
|
102
|
+
|
103
|
+
if OPTIONS[:in] == '-'
|
104
|
+
task_options[:passthrough] = [:in]
|
105
|
+
end
|
106
|
+
|
107
|
+
daemon = lambda do
|
108
|
+
Dir.chdir(OPTIONS[:root])
|
109
|
+
system("env", *ARGV)
|
110
|
+
end
|
111
|
+
|
112
|
+
task = RExec::Task.open(daemon, task_options)
|
113
|
+
|
114
|
+
if OPTIONS[:print_pid]
|
115
|
+
puts task.pid
|
116
|
+
end
|
data/lib/rexec.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
# = Summary =
|
17
|
+
# This gem provides a very simple connection based API for communicating
|
18
|
+
# with remote instances of ruby. These can either be local, or remote, such
|
19
|
+
# as over SSH.
|
20
|
+
#
|
21
|
+
# The API is very simple and deals with sending and receiving objects using
|
22
|
+
# Marshal. One of the primary goals was to impose as little structure as
|
23
|
+
# possible on the end user of this library, while still maintaining a level
|
24
|
+
# of convenience.
|
25
|
+
#
|
26
|
+
# Author:: Samuel Williams (samuel AT oriontransfer DOT org)
|
27
|
+
# Copyright:: Copyright (c) 2009 Samuel Williams.
|
28
|
+
# License:: Released under the GNU GPLv3.
|
29
|
+
|
30
|
+
require 'rexec/version'
|
31
|
+
require 'rexec/connection'
|
32
|
+
require 'rexec/server'
|
33
|
+
require 'rexec/priviledges'
|
data/lib/rexec/client.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
# This code is executed in a remote ruby process.
|
17
|
+
|
18
|
+
$stdout.sync = true
|
19
|
+
$stderr.sync = true
|
20
|
+
|
21
|
+
# We don't connect to $stderr here as this is a client. Clients write to regular $stderr.
|
22
|
+
$connection = RExec::Connection.new($stdin, $stdout)
|
23
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
# This class is as small and independant as possible as it will get sent to clients for execution.
|
17
|
+
|
18
|
+
require 'thread'
|
19
|
+
|
20
|
+
module RExec
|
21
|
+
|
22
|
+
# This class represents an abstract connection to another ruby process. The interface does not impose
|
23
|
+
# any structure on the way this communication link works, except for the fact you can send and receive
|
24
|
+
# objects. You can implement whatever kind of idiom you need for communication on top of this library.
|
25
|
+
#
|
26
|
+
# Depending on how you set things up, this can connect to a local ruby process, or a remote ruby process
|
27
|
+
# via SSH (for example).
|
28
|
+
class Connection
|
29
|
+
public
|
30
|
+
|
31
|
+
def self.build(process, options, &block)
|
32
|
+
cin = process.input
|
33
|
+
cout = process.output
|
34
|
+
cerr = process.error
|
35
|
+
|
36
|
+
# We require both cin and cout to be connected in order for connection to work
|
37
|
+
raise InvalidConnectionError.new("Input (#{cin}) or Output (#{cout}) is not connected!") unless cin and cout
|
38
|
+
|
39
|
+
yield cin
|
40
|
+
|
41
|
+
cin.puts("\004")
|
42
|
+
|
43
|
+
return self.new(cout, cin, cerr)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create a new connection. You need to supply a pipe for reading input, a pipe for sending output,
|
47
|
+
# and optionally a pipe for errors to be read from.
|
48
|
+
def initialize(input, output, error = nil)
|
49
|
+
@input = input
|
50
|
+
@output = output
|
51
|
+
@running = true
|
52
|
+
|
53
|
+
@error = error
|
54
|
+
|
55
|
+
@receive_mutex = Mutex.new
|
56
|
+
@send_mutex = Mutex.new
|
57
|
+
end
|
58
|
+
|
59
|
+
# The pipe used for reading data
|
60
|
+
def input
|
61
|
+
@input
|
62
|
+
end
|
63
|
+
|
64
|
+
# The pipe used for writing data
|
65
|
+
def output
|
66
|
+
@output
|
67
|
+
end
|
68
|
+
|
69
|
+
# The pipe used for receiving errors. On the client side this pipe is writable, on the server
|
70
|
+
# side this pipe is readable. You should avoid using it on the client side and simply use $stderr.
|
71
|
+
def error
|
72
|
+
@error
|
73
|
+
end
|
74
|
+
|
75
|
+
# Stop the connection, and close the output pipe.
|
76
|
+
def stop
|
77
|
+
@running = false
|
78
|
+
@output.close
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return whether or not the connection is running.
|
82
|
+
def running?
|
83
|
+
@running
|
84
|
+
end
|
85
|
+
|
86
|
+
# This is a very simple runloop. It provides an object when it is received.
|
87
|
+
def run(&block)
|
88
|
+
while @running
|
89
|
+
pipes = IO.select([@input])
|
90
|
+
|
91
|
+
if pipes[0].size > 0
|
92
|
+
object = receive_object
|
93
|
+
|
94
|
+
if object == nil
|
95
|
+
@running = false
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
begin
|
100
|
+
yield object
|
101
|
+
rescue Exception => ex
|
102
|
+
send_object(ex)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Dump any text which has been written to $stderr in the child process.
|
109
|
+
def dump_errors(to = $stderr)
|
110
|
+
if @error and !@error.closed?
|
111
|
+
while true
|
112
|
+
result = IO.select([@error], [], [], 0)
|
113
|
+
|
114
|
+
break if result == nil
|
115
|
+
|
116
|
+
to.puts @error.readline.chomp
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Receive an object from the connection. This function is thread-safe. This function may block.
|
122
|
+
def receive_object
|
123
|
+
object = nil
|
124
|
+
|
125
|
+
@receive_mutex.synchronize do
|
126
|
+
begin
|
127
|
+
object = Marshal.load(@input)
|
128
|
+
rescue EOFError
|
129
|
+
object = nil
|
130
|
+
@running = false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
if object and object.kind_of?(Exception)
|
135
|
+
raise object
|
136
|
+
end
|
137
|
+
|
138
|
+
return object
|
139
|
+
end
|
140
|
+
|
141
|
+
# Send object(s). This function is thread-safe.
|
142
|
+
def send_object(*objects)
|
143
|
+
@send_mutex.synchronize do
|
144
|
+
objects.each do |o|
|
145
|
+
data = Marshal.dump(o)
|
146
|
+
@output.write(data)
|
147
|
+
end
|
148
|
+
|
149
|
+
@output.flush
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/rexec/daemon.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Copyright (c) 2007, 2009 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require 'rexec/daemon/base'
|
17
|
+
|
18
|
+
module RExec
|
19
|
+
module Daemon
|
20
|
+
|
21
|
+
# Would this kind of API be useful?
|
22
|
+
#def run_daemon(options = {}, &block)
|
23
|
+
#
|
24
|
+
#end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# Copyright (c) 2007, 2009 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require 'fileutils'
|
17
|
+
require 'rexec/daemon/controller'
|
18
|
+
require 'rexec/reverse_io'
|
19
|
+
|
20
|
+
module RExec
|
21
|
+
module Daemon
|
22
|
+
# This class is the base daemon class. If you are writing a daemon, you should inherit from this class.
|
23
|
+
class Base
|
24
|
+
@@var_directory = nil
|
25
|
+
@@log_directory = nil
|
26
|
+
@@pid_directory = nil
|
27
|
+
|
28
|
+
# Return the name of the daemon
|
29
|
+
def self.daemon_name
|
30
|
+
return name.gsub(/[^a-zA-Z0-9]+/, '-')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Base directory for daemon log files / run files
|
34
|
+
def self.var_directory
|
35
|
+
@@var_directory || File.join("", "var")
|
36
|
+
end
|
37
|
+
|
38
|
+
# The directory the daemon will run in (Dir.chdir)
|
39
|
+
def self.working_directory
|
40
|
+
var_directory
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return the directory to store log files in
|
44
|
+
def self.log_directory
|
45
|
+
@@log_directory || File.join(var_directory, "log", daemon_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Standard log file for errors
|
49
|
+
def self.err_fn
|
50
|
+
File.join(log_directory, "stderr.log")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Standard log file for normal output
|
54
|
+
def self.log_fn
|
55
|
+
File.join(log_directory, "stdout.log")
|
56
|
+
end
|
57
|
+
|
58
|
+
# Standard location of pid file
|
59
|
+
def self.pid_directory
|
60
|
+
@@pid_directory || File.join(var_directory, "run", daemon_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Standard pid file
|
64
|
+
def self.pid_fn
|
65
|
+
File.join(pid_directory, "#{daemon_name}.pid")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Mark the error log
|
69
|
+
def self.mark_err_log
|
70
|
+
fp = File.open(err_fn, "a")
|
71
|
+
fp.puts "=== Error Log Opened @ #{Time.now.to_s} ==="
|
72
|
+
fp.close
|
73
|
+
end
|
74
|
+
|
75
|
+
# Prints some information relating to daemon startup problems
|
76
|
+
def self.tail_err_log(outp)
|
77
|
+
lines = []
|
78
|
+
|
79
|
+
File.open(err_fn, "r") do |fp|
|
80
|
+
fp.seek_end
|
81
|
+
|
82
|
+
fp.reverse_each_line do |line|
|
83
|
+
lines << line
|
84
|
+
break if line.match("=== Error Log") || line.match("=== Daemon Exception Backtrace")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
lines.reverse_each do |line|
|
89
|
+
outp.puts line
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check the last few lines of the log file to find out if
|
94
|
+
# the daemon crashed.
|
95
|
+
def self.crashed?
|
96
|
+
File.open(err_fn, "r") do |fp|
|
97
|
+
fp.seek_end
|
98
|
+
|
99
|
+
count = 2
|
100
|
+
fp.reverse_each_line do |line|
|
101
|
+
return true if line.match("=== Daemon Crashed")
|
102
|
+
|
103
|
+
count -= 1
|
104
|
+
|
105
|
+
break if count == 0
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return false
|
110
|
+
end
|
111
|
+
|
112
|
+
# Corresponds to controller method of the same name
|
113
|
+
def self.daemonize
|
114
|
+
Controller.daemonize(self)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Corresponds to controller method of the same name
|
118
|
+
def self.start
|
119
|
+
Controller.start(self)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Corresponds to controller method of the same name
|
123
|
+
def self.stop
|
124
|
+
Controller.stop(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Corresponds to controller method of the same name
|
128
|
+
def self.status
|
129
|
+
Controller.status(self)
|
130
|
+
end
|
131
|
+
|
132
|
+
# The main function to setup any environment required by the daemon
|
133
|
+
def self.prefork
|
134
|
+
@@var_directory = File.expand_path(@@var_directory) if @@var_directory
|
135
|
+
@@log_directory = File.expand_path(@@log_directory) if @@log_directory
|
136
|
+
@@pid_directory = File.expand_path(@@pid_directory) if @@pid_directory
|
137
|
+
|
138
|
+
FileUtils.mkdir_p(log_directory)
|
139
|
+
FileUtils.mkdir_p(pid_directory)
|
140
|
+
end
|
141
|
+
|
142
|
+
# The main function to start the daemon
|
143
|
+
def self.run
|
144
|
+
end
|
145
|
+
|
146
|
+
# The main function to stop the daemon
|
147
|
+
def self.shutdown
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|