toys 0.2.1 → 0.2.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.
data/lib/toys/context.rb CHANGED
@@ -1,16 +1,46 @@
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
+
1
32
  module Toys
33
+ ##
34
+ # The object context in effect during the execution of a tool.
35
+ #
2
36
  class Context
3
- def initialize(lookup, logger: nil, binary_name: nil, tool_name: nil, args: nil, options: nil)
4
- @_lookup = lookup
5
- @logger = logger || Logger.new(STDERR)
6
- @binary_name = binary_name
37
+ def initialize(context_base, tool_name, args, options)
38
+ @context_base = context_base
7
39
  @tool_name = tool_name
8
40
  @args = args
9
41
  @options = options
10
42
  end
11
43
 
12
- attr_reader :logger
13
- attr_reader :binary_name
14
44
  attr_reader :tool_name
15
45
  attr_reader :args
16
46
  attr_reader :options
@@ -19,21 +49,43 @@ module Toys
19
49
  @options[key]
20
50
  end
21
51
 
52
+ def logger
53
+ @context_base.logger
54
+ end
55
+
56
+ def binary_name
57
+ @context_base.binary_name
58
+ end
59
+
22
60
  def run(*args)
23
- args = args.flatten
24
- tool = @_lookup.lookup(args)
25
- tool.execute(self, args.slice(tool.full_name.length..-1))
61
+ @context_base.run(*args)
26
62
  end
27
63
 
28
64
  def exit(code)
29
65
  throw :result, code
30
66
  end
31
67
 
32
- attr_reader :_lookup
68
+ ##
69
+ # Common context data
70
+ # @private
71
+ #
72
+ class Base
73
+ def initialize(lookup, binary_name, logger)
74
+ @lookup = lookup
75
+ @binary_name = binary_name
76
+ @logger = logger || ::Logger.new(::STDERR)
77
+ end
78
+
79
+ attr_reader :binary_name
80
+ attr_reader :logger
81
+
82
+ def run(*args)
83
+ @lookup.execute(self, args.flatten)
84
+ end
33
85
 
34
- def _create_child(tool_name, args, options)
35
- Context.new(@_lookup, logger: @logger, binary_name: @binary_name,
36
- tool_name: tool_name, args: args, options: options)
86
+ def create_context(tool_name, args, options)
87
+ Context.new(self, tool_name, args, options)
88
+ end
37
89
  end
38
90
  end
39
91
  end
data/lib/toys/errors.rb CHANGED
@@ -1,7 +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
+
1
30
  module Toys
2
- class ToolDefinitionError < StandardError
31
+ ##
32
+ # An exception indicating an error in a tool definition
33
+ #
34
+ class ToolDefinitionError < ::StandardError
3
35
  end
4
36
 
5
- class LookupError < StandardError
37
+ ##
38
+ # An exception indicating a problem during tool lookup
39
+ #
40
+ class LookupError < ::StandardError
6
41
  end
7
42
  end
@@ -1,48 +1,279 @@
1
- module Toys
2
- module Helpers
3
- module Exec
4
- def config_exec(opts={})
5
- @exec_config ||= {}
6
- @exec_config.merge!(opts)
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::Helpers
33
+ ##
34
+ # A set of helper methods for invoking subcommands
35
+ #
36
+ module Exec
37
+ def configure_exec(opts = {})
38
+ @exec_config ||= {}
39
+ @exec_config.merge!(opts)
40
+ end
41
+
42
+ def exec(cmd, opts = {}, &block)
43
+ exec_opts = ExecOpts.new(self)
44
+ exec_opts.add(@exec_config) if defined? @exec_config
45
+ exec_opts.add(opts)
46
+ executor = Executor.new(exec_opts, cmd)
47
+ executor.execute(&block)
48
+ end
49
+
50
+ def ruby(args, opts = {}, &block)
51
+ cmd =
52
+ if args.is_a?(Array)
53
+ [[Exec.ruby_binary, "ruby"]] + args
54
+ else
55
+ "#{Exec.ruby_binary} #{args}"
56
+ end
57
+ exec(cmd, opts, &block)
58
+ end
59
+
60
+ def sh(cmd, opts = {})
61
+ exec(cmd, opts).exit_code
62
+ end
63
+
64
+ def capture(cmd, opts = {})
65
+ exec(cmd, opts.merge(out_to: :capture)).captured_out
66
+ end
67
+
68
+ def self.ruby_binary
69
+ ::File.join(::RbConfig::CONFIG["bindir"], ::RbConfig::CONFIG["ruby_install_name"])
70
+ end
71
+
72
+ ##
73
+ # The object passed to a subcommand control block
74
+ #
75
+ class Controller
76
+ def initialize(ins, out, err, out_err, pid)
77
+ @in = ins
78
+ @out = out
79
+ @err = err
80
+ @out_err = out_err
81
+ @pid = pid
7
82
  end
8
83
 
9
- def sh(cmd, opts={})
10
- utils = Utils.new(self, opts, @exec_config)
11
- utils.log(cmd)
12
- system(cmd)
13
- utils.handle_status($?.exitstatus)
84
+ attr_reader :in
85
+ attr_reader :out
86
+ attr_reader :err
87
+ attr_reader :out_err
88
+ attr_reader :pid
89
+
90
+ def kill(signal)
91
+ ::Process.kill(signal, pid)
14
92
  end
93
+ end
15
94
 
16
- def capture(cmd, opts={})
17
- utils = Utils.new(self, opts, @exec_config)
18
- utils.log(cmd)
19
- result = ""
95
+ ##
96
+ # The return result from a subcommand
97
+ #
98
+ class Result
99
+ def initialize(out, err, out_err, status)
100
+ @captured_out = out
101
+ @captured_err = err
102
+ @captured_out_err = out_err
103
+ @status = status
104
+ end
105
+
106
+ attr_reader :captured_out
107
+ attr_reader :captured_err
108
+ attr_reader :captured_out_err
109
+ attr_reader :status
110
+
111
+ def exit_code
112
+ status.exitstatus
113
+ end
114
+ end
115
+
116
+ ##
117
+ # An internal helper class storing the configuration of a subcommand invocation
118
+ # @private
119
+ #
120
+ class ExecOpts
121
+ ##
122
+ # Option keys that belong to exec configuration rather than spawn
123
+ # @private
124
+ #
125
+ CONFIG_KEYS = %i[
126
+ exit_on_nonzero_status
127
+ env
128
+ log_level
129
+ in_from
130
+ out_to
131
+ err_to
132
+ out_err_to
133
+ ].freeze
134
+
135
+ def initialize(context)
136
+ @context = context
137
+ @config = {}
138
+ @spawn_opts = {}
139
+ end
140
+
141
+ def add(config)
142
+ config.each do |k, v|
143
+ if CONFIG_KEYS.include?(k)
144
+ @config[k] = v
145
+ else
146
+ @spawn_opts[k] = v
147
+ end
148
+ end
149
+ end
150
+
151
+ attr_reader :config
152
+ attr_reader :spawn_opts
153
+ attr_reader :context
154
+ end
155
+
156
+ ##
157
+ # An object that manages the execution of a subcommand
158
+ # @private
159
+ #
160
+ class Executor
161
+ def initialize(exec_opts, cmd)
162
+ @cmd = Array(cmd)
163
+ @config = exec_opts.config
164
+ @context = exec_opts.context
165
+ @spawn_opts = exec_opts.spawn_opts.dup
166
+ @captures = {}
167
+ @controller_streams = {}
168
+ @join_threads = []
169
+ @child_streams = []
170
+ end
171
+
172
+ def execute(&block)
173
+ setup_in_stream
174
+ setup_out_stream(:out, :out_to, :out)
175
+ setup_out_stream(:err, :err_to, :err)
176
+ setup_out_stream(:out_err, :out_err_to, [:out, :err])
177
+ log_command
178
+ wait_thread = start_process
179
+ status = control_process(wait_thread, &block)
180
+ create_result(status)
181
+ end
182
+
183
+ private
184
+
185
+ def log_command
186
+ unless @config[:log_level] == false
187
+ cmd_str = @cmd.size == 1 ? @cmd.first : @cmd.inspect
188
+ @context.logger.add(@config[:log_level] || ::Logger::INFO, cmd_str)
189
+ end
190
+ end
191
+
192
+ def start_process
193
+ args = []
194
+ args << @config[:env] if @config[:env]
195
+ args.concat(@cmd)
196
+ pid = ::Process.spawn(*args, @spawn_opts)
197
+ @child_streams.each(&:close)
198
+ ::Process.detach(pid)
199
+ end
200
+
201
+ def control_process(wait_thread)
20
202
  begin
21
- result = `#{cmd}`
22
- utils.handle_status($?.exitstatus)
23
- rescue StandardError
24
- utils.handle_status(-1)
203
+ if block_given?
204
+ controller = Controller.new(
205
+ @controller_streams[:in], @controller_streams[:out], @controller_streams[:err],
206
+ @controller_streams[:out_err], wait_thread.pid
207
+ )
208
+ yield controller
209
+ end
210
+ ensure
211
+ @controller_streams.each_value(&:close)
25
212
  end
26
- result
213
+ @join_threads.each(&:join)
214
+ wait_thread.value
27
215
  end
28
216
 
29
- class Utils
30
- def initialize(context, opts, config)
31
- @context = context
32
- @config = config ? config.merge(opts) : opts
217
+ def create_result(status)
218
+ if @config[:exit_on_nonzero_status]
219
+ exit_status = status.exitstatus
220
+ @context.exit(exit_status) if exit_status != 0
33
221
  end
222
+ Result.new(@captures[:out], @captures[:err], @captures[:out_err], status)
223
+ end
34
224
 
35
- def log(cmd)
36
- unless @config[:log_level] == false
37
- @context.logger.add(@config[:log_level] || Logger::INFO, cmd)
225
+ def setup_in_stream
226
+ setting = @config[:in_from]
227
+ if setting
228
+ r, w = ::IO.pipe
229
+ @spawn_opts[:in] = r
230
+ w.sync = true
231
+ @child_streams << r
232
+ case setting
233
+ when :controller
234
+ @controller_streams[:in] = w
235
+ when String
236
+ write_string_thread(w, setting)
237
+ else
238
+ raise "Unknown type for in_from"
38
239
  end
39
240
  end
241
+ end
242
+
243
+ def setup_out_stream(stream_name, config_key, spawn_key)
244
+ setting = @config[config_key]
245
+ if setting
246
+ r, w = ::IO.pipe
247
+ @spawn_opts[spawn_key] = w
248
+ @child_streams << w
249
+ case setting
250
+ when :controller
251
+ @controller_streams[stream_name] = r
252
+ when :capture
253
+ @join_threads << capture_stream_thread(r, stream_name)
254
+ else
255
+ raise "Unknown type for #{config_key}"
256
+ end
257
+ end
258
+ end
259
+
260
+ def write_string_thread(stream, string)
261
+ ::Thread.new do
262
+ begin
263
+ stream.write string
264
+ ensure
265
+ stream.close
266
+ end
267
+ end
268
+ end
40
269
 
41
- def handle_status(status)
42
- if status != 0 && @config[:report_subprocess_errors]
43
- @context.exit(status)
270
+ def capture_stream_thread(stream, name)
271
+ ::Thread.new do
272
+ begin
273
+ @captures[name] = stream.read
274
+ ensure
275
+ stream.close
44
276
  end
45
- status
46
277
  end
47
278
  end
48
279
  end