toys 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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