rexec 1.1.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,192 @@
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/pidfile'
17
+ require 'rexec/task'
18
+
19
+ module RExec
20
+ module Daemon
21
+ # Daemon startup timeout
22
+ TIMEOUT = 5
23
+
24
+ # This module contains functionality related to starting and stopping the daemon, and code for processing command line input.
25
+ module Controller
26
+ # This function is called from the daemon executable. It processes ARGV and checks whether the user is asking for
27
+ # <tt>start</tt>, <tt>stop</tt>, <tt>restart</tt> or <tt>status</tt>.
28
+ def self.daemonize(daemon)
29
+ #puts "Running in #{WorkingDirectory}, logs in #{LogDirectory}"
30
+ case !ARGV.empty? && ARGV[0]
31
+ when 'start'
32
+ start(daemon)
33
+ status(daemon)
34
+ when 'stop'
35
+ stop(daemon)
36
+ status(daemon)
37
+ PidFile.cleanup(daemon)
38
+ when 'restart'
39
+ stop(daemon)
40
+ PidFile.cleanup(daemon)
41
+ start(daemon)
42
+ status(daemon)
43
+ when 'status'
44
+ status(daemon)
45
+ else
46
+ puts "Invalid command. Please specify start, restart, stop or status."
47
+ exit
48
+ end
49
+ end
50
+
51
+ # This function starts the supplied daemon
52
+ def self.start(daemon)
53
+ puts "Starting daemon..."
54
+
55
+ case PidFile.status(daemon)
56
+ when :running
57
+ $stderr.puts "Daemon already running!"
58
+ return
59
+ when :stopped
60
+ # We are good to go...
61
+ else
62
+ $stderr.puts "Daemon in unknown state! Will clear previous state and continue."
63
+ status(daemon)
64
+ PidFile.clear(daemon)
65
+ end
66
+
67
+ daemon.prefork
68
+ daemon.mark_err_log
69
+
70
+ fork do
71
+ Process.setsid
72
+ exit if fork
73
+
74
+ PidFile.store(daemon, Process.pid)
75
+
76
+ File.umask 0000
77
+ Dir.chdir daemon.working_directory
78
+
79
+ $stdin.reopen "/dev/null"
80
+ $stdout.reopen daemon.log_fn, "a"
81
+ $stderr.reopen daemon.err_fn, "a"
82
+
83
+ main = Thread.new do
84
+ begin
85
+ daemon.run
86
+ rescue
87
+ $stderr.puts "=== Daemon Exception Backtrace @ #{Time.now.to_s} ==="
88
+ $stderr.puts "#{$!.class}: #{$!.message}"
89
+ $!.backtrace.each { |at| $stderr.puts at }
90
+ $stderr.puts "=== Daemon Crashed ==="
91
+
92
+ $stderr.flush
93
+ end
94
+ end
95
+
96
+ trap("INT") do
97
+ daemon.shutdown
98
+ main.exit
99
+ end
100
+
101
+ trap("TERM") do
102
+ exit!
103
+ end
104
+
105
+ main.join
106
+ end
107
+
108
+ puts "Waiting for daemon to start..."
109
+ sleep 0.1
110
+ timer = TIMEOUT
111
+ pid = PidFile.recall(daemon)
112
+
113
+ while pid == nil and timer > 0
114
+ # Wait a moment for the forking to finish...
115
+ puts "Waiting for daemon to start (#{timer}/#{TIMEOUT})"
116
+ sleep 1
117
+
118
+ # If the daemon has crashed, it is never going to start...
119
+ break if daemon.crashed?
120
+
121
+ pid = PidFile.recall(daemon)
122
+
123
+ timer -= 1
124
+ end
125
+ end
126
+
127
+ # Prints out the status of the daemon
128
+ def self.status(daemon)
129
+ case PidFile.status(daemon)
130
+ when :running
131
+ puts "Daemon status: running pid=#{PidFile.recall(daemon)}"
132
+ when :unknown
133
+ if daemon.crashed?
134
+ puts "Daemon status: crashed"
135
+
136
+ $stdout.flush
137
+ daemon.tail_err_log($stderr)
138
+ else
139
+ puts "Daemon status: unknown"
140
+ end
141
+ when :stopped
142
+ puts "Daemon status: stopped"
143
+ end
144
+ end
145
+
146
+ # Stops the daemon process.
147
+ def self.stop(daemon)
148
+ puts "Stopping daemon..."
149
+
150
+ # Check if the pid file exists...
151
+ if !File.file?(daemon.pid_fn)
152
+ puts "Pid file not found. Is the daemon running?"
153
+ return
154
+ end
155
+
156
+ pid = PidFile.recall(daemon)
157
+
158
+ # Check if the daemon is already stopped...
159
+ unless PidFile.running(daemon)
160
+ puts "Pid #{pid} is not running. Has daemon crashed?"
161
+ return
162
+ end
163
+
164
+ pid = PidFile.recall(daemon)
165
+ Process.kill("INT", pid)
166
+ sleep 0.1
167
+
168
+ # Kill/Term loop - if the daemon didn't die easily, shoot
169
+ # it a few more times.
170
+ attempts = 5
171
+ while PidFile.running(daemon) and attempts > 0
172
+ sig = (attempts < 2) ? "KILL" : "TERM"
173
+
174
+ puts "Sending #{sig} to pid #{pid}..."
175
+ Process.kill(sig, pid)
176
+
177
+ sleep 1 unless first
178
+ attempts -= 1
179
+ end
180
+
181
+ # If after doing our best the daemon is still running (pretty odd)...
182
+ if PidFile.running(daemon)
183
+ puts "Daemon appears to be still running!"
184
+ return
185
+ end
186
+
187
+ # Otherwise the daemon has been stopped.
188
+ PidFile.clear(daemon)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,64 @@
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
+ module RExec
17
+ module Daemon
18
+ # This module controls the storage and retrieval of process id files.
19
+ module PidFile
20
+ # Saves the pid for the given daemon
21
+ def self.store(daemon, pid)
22
+ File.open(daemon.pid_fn, 'w') {|f| f << pid}
23
+ end
24
+
25
+ # Retrieves the pid for the given daemon
26
+ def self.recall(daemon)
27
+ IO.read(daemon.pid_fn).to_i rescue nil
28
+ end
29
+
30
+ # Removes the pid saved for a particular daemon
31
+ def self.clear(daemon)
32
+ if File.exist? daemon.pid_fn
33
+ FileUtils.rm(daemon.pid_fn)
34
+ end
35
+ end
36
+
37
+ # Checks whether the daemon is running by checking the saved pid and checking the corresponding process
38
+ def self.running(daemon)
39
+ pid = recall(daemon)
40
+
41
+ return false if pid == nil
42
+
43
+ gpid = Process.getpgid(pid) rescue nil
44
+
45
+ return gpid != nil ? true : false
46
+ end
47
+
48
+ # Remove the pid file if the daemon is not running
49
+ def self.cleanup(daemon)
50
+ clear(daemon) unless running(daemon)
51
+ end
52
+
53
+ # This function returns the status of the daemon. This can be one of <tt>:running</tt>, <tt>:unknown</tt> (pid file exists but no
54
+ # corresponding process can be found) or <tt>:stopped</tt>.
55
+ def self.status(daemon)
56
+ if File.exist? daemon.pid_fn
57
+ return PidFile.running(daemon) ? :running : :unknown
58
+ else
59
+ return :stopped
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+
2
+ require 'etc'
3
+
4
+ module RExec
5
+
6
+ # Set the user of the current process. Supply either a user ID
7
+ # or a user name.
8
+ #
9
+ # Be aware that on Mac OS X / Ruby 1.8 there are bugs when the user id
10
+ # is negative (i.e. it doesn't work). For example "nobody" with uid -2
11
+ # won't work.
12
+ def self.change_user(user)
13
+ if user.kind_of?(String)
14
+ user = Etc.getpwnam(user).uid
15
+ end
16
+
17
+ Process::Sys.setuid(user)
18
+ end
19
+
20
+
21
+ # Get the user of the current process. Returns the user name.
22
+ def self.current_user
23
+ uid = Process::Sys.getuid
24
+
25
+ Etc.getpwuid(uid).name
26
+ end
27
+
28
+ end
@@ -0,0 +1,70 @@
1
+
2
+ class File
3
+ # Seek to the end of the file
4
+ def seek_end
5
+ seek(0, IO::SEEK_END)
6
+ end
7
+
8
+ # Read a chunk of data and then move the file pointer backwards.
9
+ #
10
+ # Calling this function multiple times will return new data and traverse the file backwards.
11
+ #
12
+ def read_reverse(length)
13
+ offset = tell
14
+
15
+ if offset == 0
16
+ return nil
17
+ end
18
+
19
+ start = [0, offset-length].max
20
+
21
+ seek(start, IO::SEEK_SET)
22
+
23
+ buf = read(offset-start)
24
+
25
+ seek(start, IO::SEEK_SET)
26
+
27
+ return buf
28
+ end
29
+
30
+ REVERSE_BUFFER_SIZE = 128
31
+
32
+ # This function is very similar to gets but it works in reverse.
33
+ #
34
+ # You can use it to efficiently read a file line by line backwards.
35
+ #
36
+ # It returns nil when there are no more lines.
37
+ def reverse_gets(sep_string=$/)
38
+ end_pos = tell
39
+
40
+ offset = nil
41
+ buf = ""
42
+
43
+ while offset == nil
44
+ chunk = read_reverse(REVERSE_BUFFER_SIZE)
45
+ return (buf == "" ? nil : buf) if chunk == nil
46
+
47
+ buf = chunk + buf
48
+
49
+ offset = buf.rindex(sep_string)
50
+ end
51
+
52
+ line = buf[offset...buf.size].sub(sep_string, "")
53
+
54
+ seek((end_pos - buf.size) + offset, IO::SEEK_SET)
55
+
56
+ return line
57
+ end
58
+
59
+ # Similar to each_line but works in reverse. Don't forget to call
60
+ # seek_end before you start!
61
+ def reverse_each_line(sep_string=$/, &block)
62
+ line = reverse_gets(sep_string)
63
+
64
+ while line != nil
65
+ yield line
66
+
67
+ line = reverse_gets(sep_string)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,62 @@
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
+ require 'pathname'
17
+ require 'rexec/task'
18
+ require 'rexec/connection'
19
+
20
+ module RExec
21
+
22
+ class InvalidConnectionError < Exception
23
+ end
24
+
25
+ @@connection_code = (Pathname.new(__FILE__).dirname + "connection.rb").read
26
+ @@client_code = (Pathname.new(__FILE__).dirname + "client.rb").read
27
+
28
+ # Start a remote ruby server. This function is a structural cornerstone. This code runs the command you
29
+ # supply (this command should start an instance of ruby somewhere), sends it the code in
30
+ # <tt>connection.rb</tt> and <tt>client.rb</tt> as well as the code you supply.
31
+ #
32
+ # Once the remote ruby instance is set up and ready to go, this code will return (or yield) the connection
33
+ # and pid of the executed command.
34
+ #
35
+ # From this point, you can send and receive objects, and interact with the code you provided within a
36
+ # remote ruby instance.
37
+ #
38
+ # If <tt>command</tt> is a shell such as "/bin/sh", and we need to start ruby separately, you can supply
39
+ # <tt>options[:ruby] = "/usr/bin/ruby"</tt> to explicitly start the ruby command.
40
+ def self.start_server(code, command, options = {}, &block)
41
+ options[:passthrough] = :err unless options[:passthrough]
42
+
43
+ send_code = Proc.new do |cin|
44
+ cin.puts(@@connection_code)
45
+ cin.puts(@@client_code)
46
+ cin.puts(code)
47
+ end
48
+
49
+ if block_given?
50
+ Task.open(command, options) do |process|
51
+ conn = Connection.build(process, options, &send_code)
52
+
53
+ yield conn, process.pid
54
+ end
55
+ else
56
+ process = Task.open(command, options)
57
+ conn = Connection.build(process, options, &send_code)
58
+
59
+ return conn, process.pid
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,325 @@
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
+ class String
17
+ # Helper for turning a string into a shell argument
18
+ def to_arg
19
+ match(/\s/) ? dump : self
20
+ end
21
+
22
+ def to_cmd
23
+ return self
24
+ end
25
+ end
26
+
27
+ class Array
28
+ # Helper for turning an array of items into a command line string
29
+ # <tt>["ls", "-la", "/My Path"].to_cmd => "ls -la \"/My Path\""</tt>
30
+ def to_cmd
31
+ collect{ |a| a.to_arg }.join(" ")
32
+ end
33
+ end
34
+
35
+ class Pathname
36
+ # Helper for turning a pathname into a command line string
37
+ def to_cmd
38
+ to_s
39
+ end
40
+ end
41
+
42
+ module RExec
43
+ RD = 0
44
+ WR = 1
45
+
46
+ # This function closes all IO other than $stdin, $stdout, $stderr
47
+ def self.close_io(except = [$stdin, $stdout, $stderr])
48
+ # Make sure all file descriptors are closed
49
+ ObjectSpace.each_object(IO) do |io|
50
+ unless except.include?(io)
51
+ io.close rescue nil
52
+ end
53
+ end
54
+ end
55
+
56
+ class Task
57
+ private
58
+ def self.pipes_for_options(options)
59
+ pipes = [[nil, nil], [nil, nil], [nil, nil]]
60
+
61
+ if options[:passthrough]
62
+ passthrough = options[:passthrough]
63
+
64
+ if passthrough == :all
65
+ passthrough = [:in, :out, :err]
66
+ elsif passthrough.kind_of?(Symbol)
67
+ passthrough = [passthrough]
68
+ end
69
+
70
+ passthrough.each do |name|
71
+ case(name)
72
+ when :in
73
+ options[:in] = $stdin
74
+ when :out
75
+ options[:out] = $stdout
76
+ when :err
77
+ options[:err] = $stderr
78
+ end
79
+ end
80
+ end
81
+
82
+ modes = [RD, WR, WR]
83
+ {:in => 0, :out => 1, :err => 2}.each do |name, idx|
84
+ m = modes[idx]
85
+ p = options[name]
86
+
87
+ if p.kind_of?(IO)
88
+ pipes[idx][m] = p
89
+ elsif p.kind_of?(Array) and p.size == 2
90
+ pipes[idx] = p
91
+ else
92
+ pipes[idx] = IO.pipe
93
+ end
94
+ end
95
+
96
+ return pipes
97
+ end
98
+
99
+ # Close all the supplied pipes
100
+ def close_pipes(*pipes)
101
+ pipes.compact!
102
+
103
+ pipes.each do |pipe|
104
+ pipe.close unless pipe.closed?
105
+ end
106
+ end
107
+
108
+ # Dump any remaining data from the pipes, until they are closed.
109
+ def dump_pipes(*pipes)
110
+ pipes.compact!
111
+
112
+ pipes.delete_if { |pipe| pipe.closed? }
113
+ # Dump any output that was not consumed (errors, etc)
114
+ while pipes.size > 0
115
+ result = IO.select(pipes)
116
+
117
+ result[0].each do |pipe|
118
+ if pipe.closed? || pipe.eof?
119
+ pipes.delete(pipe)
120
+ next
121
+ end
122
+
123
+ $stderr.puts pipe.readline.chomp
124
+ end
125
+ end
126
+ end
127
+
128
+ public
129
+ # Returns true if the given pid is a current process
130
+ def self.running?(pid)
131
+ gpid = Process.getpgid(pid) rescue nil
132
+
133
+ return gpid != nil ? true : false
134
+ end
135
+
136
+ # Very simple method to spawn a child daemon. A daemon is detatched from the controlling tty, and thus is
137
+ # not killed when the parent process finishes.
138
+ # <tt>
139
+ # spawn_daemon do
140
+ # Dir.chdir("/")
141
+ # File.umask 0000
142
+ # puts "Hello from daemon!"
143
+ # sleep(600)
144
+ # puts "This code will not quit when parent process finishes..."
145
+ # puts "...but $stdout might be closed unless you set it to a file."
146
+ # end
147
+ # </tt>
148
+ def self.spawn_daemon(&block)
149
+ pid_pipe = IO.pipe
150
+
151
+ fork do
152
+ Process.setsid
153
+ exit if fork
154
+
155
+ # Send the pid back to the parent
156
+ pid_pipe[RD].close
157
+ pid_pipe[WR].write(Process.pid.to_s)
158
+ pid_pipe[WR].close
159
+
160
+ yield
161
+
162
+ exit(0)
163
+ end
164
+
165
+ pid_pipe[WR].close
166
+ pid = pid_pipe[RD].read
167
+ pid_pipe[RD].close
168
+
169
+ return pid.to_i
170
+ end
171
+
172
+ # Very simple method to spawn a child process
173
+ # <tt>
174
+ # spawn_child do
175
+ # puts "Hello from child!"
176
+ # end
177
+ # </tt>
178
+ def self.spawn_child(&block)
179
+ pid = fork do
180
+ yield
181
+
182
+ exit!(0)
183
+ end
184
+
185
+ return pid
186
+ end
187
+
188
+ # Open a process. Similar to IO.popen, but provides a much more generic interface to stdin, stdout,
189
+ # stderr and the pid. We also attempt to tidy up as much as possible given some kind of error or
190
+ # exception. You are expected to write to output, and read from input and error.
191
+ #
192
+ # = Options =
193
+ #
194
+ # We can specify a pipe that will be redirected to the current processes pipe. A typical one is
195
+ # :err, so that errors in the child process are printed directly to $stderr of the parent process.
196
+ # <tt>:passthrough => :err</tt>
197
+ # <tt>:passthrough => [:in, :out, :err]</tt> or <tt>:passthrough => :all</tt>
198
+ #
199
+ # We can specify a set of pipes other than the standard ones for redirecting to other things, eg
200
+ # <tt>:out => File.open("output.log", "a")</tt>
201
+ #
202
+ # If you need to supply a pipe manually, you can do that too:
203
+ # <tt>:in => IO.pipe</tt>
204
+ #
205
+ # You can specify <tt>:daemon => true</tt> to cause the child process to detatch. In this
206
+ # case you will generally want to specify files for <tt>:in, :out, :err</tt> e.g.
207
+ # <tt>
208
+ # :in => File.open("/dev/null"),
209
+ # :out => File.open("/var/log/my.log", "a"),
210
+ # :err => File.open("/var/log/my.err", "a")
211
+ # </tt>
212
+ def self.open(command, options = {}, &block)
213
+ cin, cout, cerr = pipes_for_options(options)
214
+ stdpipes = [STDIN, STDOUT, STDERR]
215
+
216
+ spawn = options[:daemonize] ? :spawn_daemon : :spawn_child
217
+
218
+ cid = self.send(spawn) do
219
+ [cin[WR], cout[RD], cerr[RD]].compact.each { |pipe| pipe.close }
220
+
221
+ STDIN.reopen(cin[RD]) if cin[RD] and !stdpipes.include?(cin[RD])
222
+ STDOUT.reopen(cout[WR]) if cout[WR] and !stdpipes.include?(cout[WR])
223
+ STDERR.reopen(cerr[WR]) if cerr[WR] and !stdpipes.include?(cerr[WR])
224
+
225
+ if command.respond_to? :call
226
+ command.call
227
+ else
228
+ # If command is a Pathname, we need to convert it to an absolute path if possible,
229
+ # otherwise if it is relative it might cause problems.
230
+ if command.respond_to? :realpath
231
+ command = command.realpath
232
+ end
233
+
234
+ if command.respond_to? :to_cmd
235
+ exec(command.to_cmd)
236
+ else
237
+ exec(command.to_s)
238
+ end
239
+ end
240
+ end
241
+
242
+ # Don't close stdin, stdout, stderr.
243
+ [cin[RD], cout[WR], cerr[WR]].compact.each { |pipe| pipe.close unless stdpipes.include?(pipe) }
244
+
245
+ task = Task.new(cin[WR], cout[RD], cerr[RD], cid)
246
+
247
+ if block_given?
248
+ begin
249
+ yield task
250
+ task.close_input
251
+ return task.wait
252
+ ensure
253
+ task.stop
254
+ end
255
+ else
256
+ return task
257
+ end
258
+ end
259
+
260
+ def initialize(input, output, error, pid)
261
+ @input = input
262
+ @output = output
263
+ @error = error
264
+
265
+ @pid = pid
266
+ @result = nil
267
+ end
268
+
269
+ attr :input
270
+ attr :output
271
+ attr :error
272
+ attr :pid
273
+ attr :result
274
+
275
+ def running?
276
+ return self.class.running?(@pid)
277
+ end
278
+
279
+ # Close all connections to the child process
280
+ def close
281
+ close_pipes(@input, @output, @error)
282
+ end
283
+
284
+ # Close input pipe to child process (if applicable)
285
+ def close_input
286
+ @input.close if @input and !@input.closed?
287
+ end
288
+
289
+ # Send a signal to the child process
290
+ def kill(signal = "INT")
291
+ Process.kill("INT", @pid)
292
+ end
293
+
294
+ # Wait for the child process to finish, return the exit status.
295
+ def wait
296
+ begin
297
+ close_input
298
+
299
+ _pid, @result = Process.wait2(@pid)
300
+
301
+ dump_pipes(@output, @error)
302
+ ensure
303
+ close_pipes(@input, @output, @error)
304
+ end
305
+
306
+ return @result
307
+ end
308
+
309
+ # Forcefully stop the child process.
310
+ def stop
311
+ # The process has already been stoped/waited upon
312
+ return if @result
313
+
314
+ begin
315
+ close_input
316
+ kill
317
+ wait
318
+
319
+ dump_pipes(@output, @error)
320
+ ensure
321
+ close_pipes(@output, @error)
322
+ end
323
+ end
324
+ end
325
+ end