rexec 1.1.10

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.
@@ -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