toys-core 0.3.4 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -32,5 +32,5 @@ module Toys
32
32
  # Current version of Toys core
33
33
  # @return [String]
34
34
  #
35
- CORE_VERSION = "0.3.4".freeze
35
+ CORE_VERSION = "0.3.5".freeze
36
36
  end
@@ -27,7 +27,7 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "logger"
30
+ require "toys/utils/exec"
31
31
 
32
32
  module Toys
33
33
  module Helpers
@@ -37,98 +37,71 @@ module Toys
37
37
  # in a string. Also provides an interface for controlling a spawned
38
38
  # process's streams.
39
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}.
40
+ # This is a frontend for {Toys::Utils::Exec}. More information is
41
+ # available in that class's documentation.
72
42
  #
73
43
  module Exec
44
+ ## @private
45
+ def self.extended(context)
46
+ context[Exec] = Utils::Exec.new do |k|
47
+ case k
48
+ when :logger
49
+ context[Context::LOGGER]
50
+ when :nonzero_status_handler
51
+ if context[Context::EXIT_ON_NONZERO_STATUS]
52
+ proc { |s| context.exit(s.exitstatus) }
53
+ end
54
+ end
55
+ end
56
+ end
57
+
74
58
  ##
75
59
  # Set default configuration keys.
76
60
  #
77
61
  # @param [Hash] opts The default options. See the section on
78
- # configuration options in the {Toys::Helpers::Exec} module docs.
62
+ # configuration options in the {Toys::Utils::Exec} docs.
79
63
  #
80
64
  def configure_exec(opts = {})
81
- @exec_config ||= {}
82
- @exec_config.merge!(opts)
65
+ self[Exec].config_defaults(opts)
83
66
  end
84
67
 
85
68
  ##
86
69
  # Execute a command. The command may be given as a single string to pass
87
70
  # to a shell, or an array of strings indicating a posix command.
88
71
  #
89
- # If you provide a block, a {Toys::Helpers::Exec::Controller} will be
72
+ # If you provide a block, a {Toys::Utils::Exec::Controller} will be
90
73
  # yielded to it, allowing you to interact with the subprocess streams.
91
74
  #
92
75
  # @param [String,Array<String>] cmd The command to execute.
93
76
  # @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.
77
+ # configuration options in the {Toys::Utils::Exec} module docs.
78
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
79
+ # the subprocess streams.
97
80
  #
98
- # @return [Toys::Helpers::Result] The subprocess result, including
99
- # exit code and any captured output.
81
+ # @return [Toys::Utils::Result] The subprocess result, including the exit
82
+ # code and any captured output.
100
83
  #
101
84
  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)
85
+ self[Exec].exec(cmd, Exec._setup_exec_opts(opts, self), &block)
107
86
  end
108
87
 
109
88
  ##
110
89
  # Spawn a ruby process and pass the given arguments to it.
111
90
  #
112
- # If you provide a block, a {Toys::Helpers::Exec::Controller} will be
91
+ # If you provide a block, a {Toys::Utils::Exec::Controller} will be
113
92
  # yielded to it, allowing you to interact with the subprocess streams.
114
93
  #
115
94
  # @param [String,Array<String>] args The arguments to ruby.
116
95
  # @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
96
+ # configuration options in the {Toys::Utils::Exec} module docs.
97
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
119
98
  # for the subprocess streams.
120
99
  #
121
- # @return [Toys::Helpers::Result] The subprocess result, including
122
- # exit code and any captured output.
100
+ # @return [Toys::Utils::Result] The subprocess result, including the exit
101
+ # code and any captured output.
123
102
  #
124
103
  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)
104
+ self[Exec].ruby(args, Exec._setup_exec_opts(opts, self), &block)
132
105
  end
133
106
 
134
107
  ##
@@ -136,14 +109,14 @@ module Toys
136
109
  #
137
110
  # @param [String] cmd The shell command to execute.
138
111
  # @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.
112
+ # configuration options in the {Toys::Utils::Exec} module docs.
113
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
114
+ # the subprocess streams.
142
115
  #
143
116
  # @return [Integer] The exit code
144
117
  #
145
118
  def sh(cmd, opts = {})
146
- exec(cmd, opts).exit_code
119
+ self[Exec].sh(cmd, Exec._setup_exec_opts(opts, self))
147
120
  end
148
121
 
149
122
  ##
@@ -154,315 +127,24 @@ module Toys
154
127
  #
155
128
  # @param [String,Array<String>] cmd The command to execute.
156
129
  # @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.
130
+ # configuration options in the {Toys::Utils::Exec} module docs.
131
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
132
+ # the subprocess streams.
160
133
  #
161
134
  # @return [String] What was written to standard out.
162
135
  #
163
136
  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
137
+ self[Exec].capture(cmd, Exec._setup_exec_opts(opts, self))
301
138
  end
302
139
 
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
140
+ ## @private
141
+ def self._setup_exec_opts(opts, context)
142
+ return opts unless opts.key?(:exit_on_nonzero_status)
143
+ nonzero_status_handler =
144
+ if opts[:exit_on_nonzero_status]
145
+ proc { |s| context.exit(s.exitstatus) }
454
146
  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
147
+ opts.merge(nonzero_status_handler: nonzero_status_handler)
466
148
  end
467
149
  end
468
150
  end