toys-core 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ module Toys
31
+ ##
32
+ # Current version of Toys core
33
+ # @return [String]
34
+ #
35
+ CORE_VERSION = "0.3.2".freeze
36
+ end
@@ -0,0 +1,42 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ module Toys
31
+ ##
32
+ # An exception indicating an error in a tool definition
33
+ #
34
+ class ToolDefinitionError < ::StandardError
35
+ end
36
+
37
+ ##
38
+ # An exception indicating a problem during tool lookup
39
+ #
40
+ class LoaderError < ::StandardError
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ require "toys/utils/module_lookup"
31
+
32
+ module Toys
33
+ ##
34
+ # Namespace for common helper modules
35
+ #
36
+ module Helpers
37
+ ##
38
+ # Return a helper module by name.
39
+ #
40
+ # Currently recognized module names are:
41
+ #
42
+ # * `:exec` : Methods to help execute subcommands.
43
+ # * `:file_utils` : The FileUtils standard library methods.
44
+ #
45
+ # @param [String,Symbol] name Name of the helper module to return
46
+ # @return [Module,nil] The module, or `nil` if not found
47
+ #
48
+ def self.lookup(name)
49
+ Utils::ModuleLookup.lookup(:helpers, name)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,469 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ require "logger"
31
+
32
+ module Toys
33
+ module Helpers
34
+ ##
35
+ # A set of helper methods for invoking subcommands. Provides shortcuts for
36
+ # common cases such as invoking Ruby in a subprocess or capturing output
37
+ # in a string. Also provides an interface for controlling a spawned
38
+ # process's streams.
39
+ #
40
+ # ## Configuration options
41
+ #
42
+ # A variety of options can be used to control subprocesses. These include:
43
+ #
44
+ # * **:env** (Hash) Environment variables to pass to the subprocess
45
+ # * **:log_level** (Integer) If set, the actual command will be logged
46
+ # at the given level.
47
+ # * **:in_from** (`:controller`,String) Connects the input stream of the
48
+ # subprocess. If set to `:controller`, the controller will control the
49
+ # input stream. If set to a string, that string will be written to the
50
+ # input stream. If not set, the input stream will be connected to the
51
+ # STDIN for the Toys process itself.
52
+ # * **:out_to** (`:controller`,`:capture`) Connects the standard output
53
+ # stream of the subprocess. If set to `:controller`, the controller
54
+ # will control the output stream. If set to `:capture`, the output will
55
+ # be captured in a string that is available in the
56
+ # {Toys::Helpers::Exec::Result} object. If not set, the subprocess
57
+ # standard out is connected to STDOUT of the Toys process.
58
+ # * **:err_to** (`:controller`,`:capture`) Connects the standard error
59
+ # stream of the subprocess. See `:out_to` for more details.
60
+ # * **:out_err_to** (`:controller`,`:capture`) Combines the standard out
61
+ # and error streams of the subprocess and connects them. See `:out_to`
62
+ # for more details.
63
+ # * **:exit_on_nonzero_status** (Boolean) If true, a nonzero status code
64
+ # will cause the entire tool to terminate. Default is false.
65
+ #
66
+ # In addition, any options recognized by `Process#spawn` are supported.
67
+ # These include `:umask`, `:pgroup`, `:chdir`, and many others.
68
+ #
69
+ # Configuration options may be provided to any method that starts a
70
+ # subprocess. You may also set default values for this tool by calling
71
+ # {Toys::Helpers::Exec#configure_exec}.
72
+ #
73
+ module Exec
74
+ ##
75
+ # Set default configuration keys.
76
+ #
77
+ # @param [Hash] opts The default options. See the section on
78
+ # configuration options in the {Toys::Helpers::Exec} module docs.
79
+ #
80
+ def configure_exec(opts = {})
81
+ @exec_config ||= {}
82
+ @exec_config.merge!(opts)
83
+ end
84
+
85
+ ##
86
+ # Execute a command. The command may be given as a single string to pass
87
+ # to a shell, or an array of strings indicating a posix command.
88
+ #
89
+ # If you provide a block, a {Toys::Helpers::Exec::Controller} will be
90
+ # yielded to it, allowing you to interact with the subprocess streams.
91
+ #
92
+ # @param [String,Array<String>] cmd The command to execute.
93
+ # @param [Hash] opts The command options. See the section on
94
+ # configuration options in the {Toys::Helpers::Exec} module docs.
95
+ # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
96
+ # for the subprocess streams.
97
+ #
98
+ # @return [Toys::Helpers::Result] The subprocess result, including
99
+ # exit code and any captured output.
100
+ #
101
+ def exec(cmd, opts = {}, &block)
102
+ exec_opts = ExecOpts.new(self)
103
+ exec_opts.add(@exec_config) if defined? @exec_config
104
+ exec_opts.add(opts)
105
+ executor = Executor.new(exec_opts, cmd)
106
+ executor.execute(&block)
107
+ end
108
+
109
+ ##
110
+ # Spawn a ruby process and pass the given arguments to it.
111
+ #
112
+ # If you provide a block, a {Toys::Helpers::Exec::Controller} will be
113
+ # yielded to it, allowing you to interact with the subprocess streams.
114
+ #
115
+ # @param [String,Array<String>] args The arguments to ruby.
116
+ # @param [Hash] opts The command options. See the section on
117
+ # configuration options in the {Toys::Helpers::Exec} module docs.
118
+ # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
119
+ # for the subprocess streams.
120
+ #
121
+ # @return [Toys::Helpers::Result] The subprocess result, including
122
+ # exit code and any captured output.
123
+ #
124
+ def ruby(args, opts = {}, &block)
125
+ cmd =
126
+ if args.is_a?(Array)
127
+ [[Exec.ruby_binary, "ruby"]] + args
128
+ else
129
+ "#{Exec.ruby_binary} #{args}"
130
+ end
131
+ exec(cmd, opts, &block)
132
+ end
133
+
134
+ ##
135
+ # Execute the given string in a shell. Returns the exit code.
136
+ #
137
+ # @param [String] cmd The shell command to execute.
138
+ # @param [Hash] opts The command options. See the section on
139
+ # configuration options in the {Toys::Helpers::Exec} module docs.
140
+ # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
141
+ # for the subprocess streams.
142
+ #
143
+ # @return [Integer] The exit code
144
+ #
145
+ def sh(cmd, opts = {})
146
+ exec(cmd, opts).exit_code
147
+ end
148
+
149
+ ##
150
+ # Execute a command. The command may be given as a single string to pass
151
+ # to a shell, or an array of strings indicating a posix command.
152
+ #
153
+ # Captures standard out and returns it as a string.
154
+ #
155
+ # @param [String,Array<String>] cmd The command to execute.
156
+ # @param [Hash] opts The command options. See the section on
157
+ # configuration options in the {Toys::Helpers::Exec} module docs.
158
+ # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
159
+ # for the subprocess streams.
160
+ #
161
+ # @return [String] What was written to standard out.
162
+ #
163
+ def capture(cmd, opts = {})
164
+ exec(cmd, opts.merge(out_to: :capture)).captured_out
165
+ end
166
+
167
+ ##
168
+ # Returns the paty to the Ruby binary
169
+ # @return [String] Path to the Ruby binary
170
+ #
171
+ def self.ruby_binary
172
+ ::File.join(::RbConfig::CONFIG["bindir"], ::RbConfig::CONFIG["ruby_install_name"])
173
+ end
174
+
175
+ ##
176
+ # An object of this type is passed to a subcommand control block.
177
+ # You may use this object to interact with the subcommand's streams,
178
+ # and/or send signals to the process.
179
+ #
180
+ class Controller
181
+ ## @private
182
+ def initialize(ins, out, err, out_err, pid)
183
+ @in = ins
184
+ @out = out
185
+ @err = err
186
+ @out_err = out_err
187
+ @pid = pid
188
+ end
189
+
190
+ ##
191
+ # Return the subcommand's standard input stream (which can be written
192
+ # to), if the command was configured with `in_from: :controller`.
193
+ # Returns `nil` otherwise.
194
+ # @return [IO,nil]
195
+ #
196
+ attr_reader :in
197
+
198
+ ##
199
+ # Return the subcommand's standard output stream (which can be read
200
+ # from), if the command was configured with `out_to: :controller`.
201
+ # Returns `nil` otherwise.
202
+ # @return [IO,nil]
203
+ #
204
+ attr_reader :out
205
+
206
+ ##
207
+ # Return the subcommand's standard error stream (which can be read
208
+ # from), if the command was configured with `err_to: :controller`.
209
+ # Returns `nil` otherwise.
210
+ # @return [IO,nil]
211
+ #
212
+ attr_reader :err
213
+
214
+ ##
215
+ # Return the subcommand's combined standard output and error stream
216
+ # (which can be read from), if the command was configured with
217
+ # `out_err_to: :controller`. Returns `nil` otherwise.
218
+ # @return [IO,nil]
219
+ #
220
+ attr_reader :out_err
221
+
222
+ ##
223
+ # Returns the process ID.
224
+ # @return [Integer]
225
+ #
226
+ attr_reader :pid
227
+
228
+ ##
229
+ # Send the given signal to the process. The signal may be specified
230
+ # by name or number.
231
+ #
232
+ # @param [Integer,String] signal The signal to send.
233
+ #
234
+ def kill(signal)
235
+ ::Process.kill(signal, pid)
236
+ end
237
+ end
238
+
239
+ ##
240
+ # The return result from a subcommand
241
+ #
242
+ class Result
243
+ ## @private
244
+ def initialize(out, err, out_err, status)
245
+ @captured_out = out
246
+ @captured_err = err
247
+ @captured_out_err = out_err
248
+ @status = status
249
+ end
250
+
251
+ ##
252
+ # Returns the captured output string, if the command was configured
253
+ # with `out_to: :capture`. Returns `nil` otherwise.
254
+ # @return [String,nil]
255
+ #
256
+ attr_reader :captured_out
257
+
258
+ ##
259
+ # Returns the captured error string, if the command was configured
260
+ # with `err_to: :capture`. Returns `nil` otherwise.
261
+ # @return [String,nil]
262
+ #
263
+ attr_reader :captured_err
264
+
265
+ ##
266
+ # Returns the captured combined output and error string, if the command
267
+ # was configured with `out_err_to: :capture`. Returns `nil` otherwise.
268
+ # @return [String,nil]
269
+ #
270
+ attr_reader :captured_out_err
271
+
272
+ ##
273
+ # Returns the status code object.
274
+ # @return [Process::Status]
275
+ #
276
+ attr_reader :status
277
+
278
+ ##
279
+ # Returns the numeric status code.
280
+ # @return [Integer]
281
+ #
282
+ def exit_code
283
+ status.exitstatus
284
+ end
285
+
286
+ ##
287
+ # Returns true if the subprocess terminated with a zero status.
288
+ # @return [Boolean]
289
+ #
290
+ def success?
291
+ exit_code.zero?
292
+ end
293
+
294
+ ##
295
+ # Returns true if the subprocess terminated with a nonzero status.
296
+ # @return [Boolean]
297
+ #
298
+ def error?
299
+ !exit_code.zero?
300
+ end
301
+ end
302
+
303
+ ##
304
+ # An internal helper class storing the configuration of a subcommand invocation
305
+ # @private
306
+ #
307
+ class ExecOpts
308
+ ##
309
+ # Option keys that belong to exec configuration rather than spawn
310
+ # @private
311
+ #
312
+ CONFIG_KEYS = %i[
313
+ exit_on_nonzero_status
314
+ env
315
+ log_level
316
+ in_from
317
+ out_to
318
+ err_to
319
+ out_err_to
320
+ ].freeze
321
+
322
+ def initialize(context)
323
+ @context = context
324
+ @config = {exit_on_nonzero_status: @context.get(Context::EXIT_ON_NONZERO_STATUS)}
325
+ @spawn_opts = {}
326
+ end
327
+
328
+ def add(config)
329
+ config.each do |k, v|
330
+ if CONFIG_KEYS.include?(k)
331
+ @config[k] = v
332
+ else
333
+ @spawn_opts[k] = v
334
+ end
335
+ end
336
+ end
337
+
338
+ attr_reader :config
339
+ attr_reader :spawn_opts
340
+ attr_reader :context
341
+ end
342
+
343
+ ##
344
+ # An object that manages the execution of a subcommand
345
+ # @private
346
+ #
347
+ class Executor
348
+ def initialize(exec_opts, cmd)
349
+ @cmd = Array(cmd)
350
+ @config = exec_opts.config
351
+ @context = exec_opts.context
352
+ @spawn_opts = exec_opts.spawn_opts.dup
353
+ @captures = {}
354
+ @controller_streams = {}
355
+ @join_threads = []
356
+ @child_streams = []
357
+ end
358
+
359
+ def execute(&block)
360
+ setup_in_stream
361
+ setup_out_stream(:out, :out_to, :out)
362
+ setup_out_stream(:err, :err_to, :err)
363
+ setup_out_stream(:out_err, :out_err_to, [:out, :err])
364
+ log_command
365
+ wait_thread = start_process
366
+ status = control_process(wait_thread, &block)
367
+ create_result(status)
368
+ end
369
+
370
+ private
371
+
372
+ def log_command
373
+ unless @config[:log_level] == false
374
+ cmd_str = @cmd.size == 1 ? @cmd.first : @cmd.inspect
375
+ @context.logger.add(@config[:log_level] || ::Logger::INFO, cmd_str)
376
+ end
377
+ end
378
+
379
+ def start_process
380
+ args = []
381
+ args << @config[:env] if @config[:env]
382
+ args.concat(@cmd)
383
+ pid = ::Process.spawn(*args, @spawn_opts)
384
+ @child_streams.each(&:close)
385
+ ::Process.detach(pid)
386
+ end
387
+
388
+ def control_process(wait_thread)
389
+ begin
390
+ if block_given?
391
+ controller = Controller.new(
392
+ @controller_streams[:in], @controller_streams[:out], @controller_streams[:err],
393
+ @controller_streams[:out_err], wait_thread.pid
394
+ )
395
+ yield controller
396
+ end
397
+ ensure
398
+ @controller_streams.each_value(&:close)
399
+ end
400
+ @join_threads.each(&:join)
401
+ wait_thread.value
402
+ end
403
+
404
+ def create_result(status)
405
+ if @config[:exit_on_nonzero_status]
406
+ exit_status = status.exitstatus
407
+ @context.exit(exit_status) if exit_status != 0
408
+ end
409
+ Result.new(@captures[:out], @captures[:err], @captures[:out_err], status)
410
+ end
411
+
412
+ def setup_in_stream
413
+ setting = @config[:in_from]
414
+ if setting
415
+ r, w = ::IO.pipe
416
+ @spawn_opts[:in] = r
417
+ w.sync = true
418
+ @child_streams << r
419
+ case setting
420
+ when :controller
421
+ @controller_streams[:in] = w
422
+ when String
423
+ write_string_thread(w, setting)
424
+ else
425
+ raise "Unknown type for in_from"
426
+ end
427
+ end
428
+ end
429
+
430
+ def setup_out_stream(stream_name, config_key, spawn_key)
431
+ setting = @config[config_key]
432
+ if setting
433
+ r, w = ::IO.pipe
434
+ @spawn_opts[spawn_key] = w
435
+ @child_streams << w
436
+ case setting
437
+ when :controller
438
+ @controller_streams[stream_name] = r
439
+ when :capture
440
+ @join_threads << capture_stream_thread(r, stream_name)
441
+ else
442
+ raise "Unknown type for #{config_key}"
443
+ end
444
+ end
445
+ end
446
+
447
+ def write_string_thread(stream, string)
448
+ ::Thread.new do
449
+ begin
450
+ stream.write string
451
+ ensure
452
+ stream.close
453
+ end
454
+ end
455
+ end
456
+
457
+ def capture_stream_thread(stream, name)
458
+ ::Thread.new do
459
+ begin
460
+ @captures[name] = stream.read
461
+ ensure
462
+ stream.close
463
+ end
464
+ end
465
+ end
466
+ end
467
+ end
468
+ end
469
+ end