toys-core 0.3.2

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,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