cheetah 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +37 -2
  3. data/README.md +94 -14
  4. data/VERSION +1 -1
  5. data/lib/cheetah/version.rb +4 -1
  6. data/lib/cheetah.rb +242 -107
  7. metadata +18 -43
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bd4cbe464be1d5ee56434c1a380d958cf0cbcb40095d8f21fcfb3cf7b2320db2
4
+ data.tar.gz: 3572805d086b24911c1feb6169b5911723a918fcf1ca0547d2f28984e0827ade
5
+ SHA512:
6
+ metadata.gz: 032f86dbf253379e1663e01d03f8a5a013a13829e4bb5b9c3f880d73611a2c92176e51086402b604cfd20a30d28dfa1571766b9fa2e4eb5377e4c12a6bda1b39
7
+ data.tar.gz: 1ecf8c2ff66d2eef73b658f925bbb5a7c6292861ce4fd52a9f2ed6fab91e49f3ba25fe5cd84f8da294b6340887872c9ef5100f2799fa8f8c319afbcead8ced3e
data/CHANGELOG CHANGED
@@ -1,11 +1,46 @@
1
+ 1.0.0 (2021-11-30)
2
+ ------------------
3
+
4
+ * Add support for ruby 3.0
5
+ As side effect now Recorder#record_status receive additional parameter
6
+
7
+ 0.5.2 (2020-01-06)
8
+ ------------------
9
+
10
+ * If listed in allowed_exitstatus, log exit code as Info, not as Error
11
+ (bsc#1153749)
12
+ * Added support for ruby 2.7
13
+
14
+ 0.5.1 (2019-10-16)
15
+ ------------------
16
+
17
+ * Implement closing open fds after call to fork (bsc#1151960). This will work
18
+ only in linux system with mounted /proc. For other Unixes it works as before.
19
+ * drop support for ruby that is EOL (2.3 and lower)
20
+ * Added support for ruby 2.4, 2.5, 2.6
21
+
22
+ 0.5.0 (2015-12-18)
23
+ ------------------
24
+
25
+ * Added chroot option for executing in different system root.
26
+ * Added ENV overwrite option.
27
+ * Allowed to specify known exit codes that are not errors.
28
+ * Documented how to execute in different working directory.
29
+ * Allowed passing nil as :stdin to be same as :stdout and :strerr.
30
+ * Converted parameters for command to strings with `.to_s`.
31
+ * Adapted testsuite to new rspec.
32
+ * Updated documentation with various fixes.
33
+ * Dropped support for Ruby 1.9.3.
34
+ * Added support for Ruby 2.1 and 2.2.
35
+
1
36
  0.4.0 (2013-11-21)
2
37
  ------------------
3
38
 
4
39
  * Implemented incremental logging. The input and both outputs of the executed
5
40
  command are now logged one-by-line by the default recorder. A custom recorder
6
41
  can record them on even finer granularity.
7
- * Dropped support of Ruby 1.8.7.
8
- * Added support of Ruby 2.0.0.
42
+ * Dropped support for Ruby 1.8.7.
43
+ * Added support for Ruby 2.0.0.
9
44
  * Internal code improvements.
10
45
 
11
46
  0.3.0 (2012-06-21)
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  Cheetah
2
2
  =======
3
+ [![Travis Build](https://travis-ci.org/openSUSE/cheetah.svg?branch=master)](https://travis-ci.org/openSUSE/cheetah)
4
+ [![Code Climate](https://codeclimate.com/github/openSUSE/cheetah/badges/gpa.svg)](https://codeclimate.com/github/openSUSE/cheetah)
5
+ [![Coverage Status](https://img.shields.io/coveralls/openSUSE/cheetah.svg)](https://coveralls.io/r/openSUSE/cheetah?branch=master)
6
+
3
7
 
4
8
  Your swiss army knife for executing external commands in Ruby safely and
5
9
  conveniently.
@@ -9,11 +13,11 @@ Examples
9
13
 
10
14
  ```ruby
11
15
  # Run a command and capture its output
12
- files = Cheetah.run("ls", "-la", :stdout => :capture)
16
+ files = Cheetah.run("ls", "-la", stdout: :capture)
13
17
 
14
18
  # Run a command and capture its output into a stream
15
19
  File.open("files.txt", "w") do |stdout|
16
- Cheetah.run("ls", "-la", :stdout => stdout)
20
+ Cheetah.run("ls", "-la", stdout: stdout)
17
21
  end
18
22
 
19
23
  # Run a command and handle errors
@@ -22,7 +26,7 @@ begin
22
26
  rescue Cheetah::ExecutionFailed => e
23
27
  puts e.message
24
28
  puts "Standard output: #{e.stdout}"
25
- puts "Error ouptut: #{e.stderr}"
29
+ puts "Error output: #{e.stderr}"
26
30
  end
27
31
  ```
28
32
 
@@ -34,7 +38,11 @@ Features
34
38
  * Piping commands together
35
39
  * 100% secure (shell expansion is impossible by design)
36
40
  * Raises exceptions on errors (no more manual status code checks)
41
+ but allows to specify which non-zero codes are not an error
42
+ * Thread-safety
43
+ * Allows overriding environment variables
37
44
  * Optional logging for easy debugging
45
+ * Running on changed root ( requires chroot permission )
38
46
 
39
47
  Non-features
40
48
  ------------
@@ -63,13 +71,16 @@ To run a command, just specify it together with its arguments:
63
71
 
64
72
  ```ruby
65
73
  Cheetah.run("tar", "xzf", "foo.tar.gz")
74
+
75
+ Cheetah converts each argument to a string using `#to_s`.
76
+
66
77
  ```
67
78
  ### Passing Input
68
79
 
69
80
  Using the `:stdin` option you can pass a string to command's standard input:
70
81
 
71
82
  ```ruby
72
- Cheetah.run("python", :stdin => source_code)
83
+ Cheetah.run("python", stdin: source_code)
73
84
  ```
74
85
 
75
86
  If the input is big you may want to avoid passing it in one huge string. In that
@@ -78,7 +89,7 @@ input from it gradually.
78
89
 
79
90
  ```ruby
80
91
  File.open("huge_program.py") do |stdin|
81
- Cheetah.run("python", :stdin => stdin)
92
+ Cheetah.run("python", stdin: stdin)
82
93
  end
83
94
  ```
84
95
 
@@ -88,7 +99,7 @@ To capture command's standard output, set the `:stdout` option to `:capture`.
88
99
  You will receive the output as a return value of the call:
89
100
 
90
101
  ```ruby
91
- files = Cheetah.run("ls", "-la", :stdout => :capture)
102
+ files = Cheetah.run("ls", "-la", stdout: :capture)
92
103
  ```
93
104
 
94
105
  The same technique works with the error output — just use the `:stderr` option.
@@ -96,7 +107,7 @@ If you specify capturing of both outputs, the return value will be a two-element
96
107
  array:
97
108
 
98
109
  ```ruby
99
- results, errors = Cheetah.run("grep", "-r", "User", ".", :stdout => :capture, :stderr => :capture)
110
+ results, errors = Cheetah.run("grep", "-r", "User", ".", stdout: => :capture, stderr: => :capture)
100
111
  ```
101
112
 
102
113
  If the output is big you may want to avoid capturing it into a huge string. In
@@ -105,7 +116,7 @@ command will write its output into it gradually.
105
116
 
106
117
  ```ruby
107
118
  File.open("files.txt", "w") do |stdout|
108
- Cheetah.run("ls", "-la", :stdout => stdout)
119
+ Cheetah.run("ls", "-la", stdout: stdout)
109
120
  end
110
121
  ```
111
122
 
@@ -115,12 +126,12 @@ You can pipe multiple commands together and execute them as one. Just specify
115
126
  the commands together with their arguments as arrays:
116
127
 
117
128
  ```ruby
118
- processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], :stdout => :capture)
129
+ processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], stdout: :capture)
119
130
  ```
120
131
 
121
132
  ### Error Handling
122
133
 
123
- If the command can't be executed for some reason or returns a non-zero exit
134
+ If the command can't be executed for some reason or returns an unexpected non-zero exit
124
135
  status, Cheetah raises an exception with detailed information about the failure:
125
136
 
126
137
  ```ruby
@@ -130,7 +141,8 @@ begin
130
141
  rescue Cheetah::ExecutionFailed => e
131
142
  puts e.message
132
143
  puts "Standard output: #{e.stdout}"
133
- puts "Error ouptut: #{e.stderr}"
144
+ puts "Error output: #{e.stderr}"
145
+ puts "Exit status: #{e.status.exitstatus}"
134
146
  end
135
147
  ```
136
148
  ### Logging
@@ -139,7 +151,55 @@ For debugging purposes, you can use a logger. Cheetah will log the command, its
139
151
  status, input and both outputs to it:
140
152
 
141
153
  ```ruby
142
- Cheetah.run("ls -l", :logger => logger)
154
+ Cheetah.run("ls -l", logger: logger)
155
+ ```
156
+
157
+ ### Overwriting env
158
+
159
+ If the command needs adapted environment variables, use the :env option.
160
+ Passed hash is used to update existing env (for details see ENV.update).
161
+ Nil value means unset variable. Environment is restored to its original state after
162
+ running the command.
163
+
164
+ ```ruby
165
+ Cheetah.run("env", env: { "LC_ALL" => "C" })
166
+ ```
167
+
168
+ ### Expecting Non-zero Exit Status
169
+
170
+ If command is expected to return valid a non-zero exit status like `grep` command
171
+ which return `1` if given regexp is not found, then option `:allowed_exitstatus`
172
+ can be used:
173
+
174
+ ```ruby
175
+ # Run a command, handle exitstatus and handle errors
176
+ begin
177
+ exitstatus = Cheetah.run("grep", "userA", "/etc/passwd", allowed_exitstatus: 1)
178
+ if exitstates == 0
179
+ puts "found"
180
+ else
181
+ puts "not found"
182
+ end
183
+ rescue Cheetah::ExecutionFailed => e
184
+ puts e.message
185
+ puts "Standard output: #{e.stdout}"
186
+ puts "Error output: #{e.stderr}"
187
+ puts "Exit status: #{e.status.exitstatus}"
188
+ end
189
+ ```
190
+
191
+ Exit status is returned as last element of result. If it is only captured thing,
192
+ then it is return without array.
193
+ Supported input for `allowed_exitstatus` are anything supporting include, fixnum
194
+ or nil for no allowed existatus.
195
+
196
+ ```ruby
197
+ # allowed inputs
198
+ allowed_exitstatus: 1
199
+ allowed_exitstatus: 1..5
200
+ allowed_exitstatus: [1, 2]
201
+ allowed_exitstatus: object_with_include_method
202
+ allowed_exitstatus: nil
143
203
  ```
144
204
 
145
205
  ### Setting Defaults
@@ -149,13 +209,33 @@ To avoid repetition, you can set global default value of any option passed too
149
209
 
150
210
  ```ruby
151
211
  # If you're tired of passing the :logger option all the time...
152
- Cheetah.default_options = { :logger = my_logger }
212
+ Cheetah.default_options = { :logger => my_logger }
153
213
  Cheetah.run("./configure")
154
214
  Cheetah.run("make")
155
215
  Cheetah.run("make", "install")
156
216
  Cheetah.default_options = {}
157
217
  ```
158
218
 
219
+ ### Changing Working Directory
220
+
221
+ If diferent working directory is needed for running program, then suggested
222
+ usage is to enclose call into `Dir.chdir` method.
223
+
224
+ ```ruby
225
+ Dir.chdir("/workspace") do
226
+ Cheetah.run("make")
227
+ end
228
+ ```
229
+
230
+ ### Changing System Root
231
+
232
+ If a command needs to be executed in different system root then the `:chroot`
233
+ option can be used:
234
+
235
+ ```ruby
236
+ Cheetah.run("/usr/bin/inspect", chroot: "/mnt/target_system")
237
+ ```
238
+
159
239
  ### More Information
160
240
 
161
241
  For more information, see the
@@ -164,7 +244,7 @@ For more information, see the
164
244
  Compatibility
165
245
  -------------
166
246
 
167
- Cheetah should run well on any Unix system with Ruby 1.9.3 or 2.0.0. Non-Unix
247
+ Cheetah should run well on any Unix system with Ruby 2.0.0, 2.1 and 2.2. Non-Unix
168
248
  systems and different Ruby implementations/versions may work too but they were
169
249
  not tested.
170
250
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 1.0.0
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Cheetah namespace
1
4
  module Cheetah
2
5
  # Cheetah version (uses [semantic versioning](http://semver.org/)).
3
- VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").strip
6
+ VERSION = File.read("#{File.dirname(__FILE__)}/../../VERSION").strip
4
7
  end
data/lib/cheetah.rb CHANGED
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "abstract_method"
2
4
  require "logger"
3
5
  require "shellwords"
4
6
  require "stringio"
5
7
 
6
- require File.expand_path(File.dirname(__FILE__) + "/cheetah/version")
8
+ require File.expand_path("#{File.dirname(__FILE__)}/cheetah/version")
7
9
 
8
10
  # Your swiss army knife for executing external commands in Ruby safely and
9
11
  # conveniently.
@@ -22,11 +24,11 @@ require File.expand_path(File.dirname(__FILE__) + "/cheetah/version")
22
24
  # * Handling of interactive commands
23
25
  #
24
26
  # @example Run a command and capture its output
25
- # files = Cheetah.run("ls", "-la", :stdout => :capture)
27
+ # files = Cheetah.run("ls", "-la", stdout: :capture)
26
28
  #
27
29
  # @example Run a command and capture its output into a stream
28
30
  # File.open("files.txt", "w") do |stdout|
29
- # Cheetah.run("ls", "-la", :stdout => stdout)
31
+ # Cheetah.run("ls", "-la", stdout: stdout)
30
32
  # end
31
33
  #
32
34
  # @example Run a command and handle errors
@@ -119,17 +121,22 @@ module Cheetah
119
121
  #
120
122
  # @abstract
121
123
  # @param [Process::Status] status the executed command exit status
124
+ # @param [Boolean] allowed_status whether the exit code is in the list of allowed exit codes
122
125
  abstract_method :record_status
123
126
  end
124
127
 
125
128
  # A recorder that does not record anyting. Used by {Cheetah.run} when no
126
129
  # logger is passed.
127
130
  class NullRecorder < Recorder
128
- def record_commands(commands); end
129
- def record_stdin(stdin); end
130
- def record_stdout(stdout); end
131
- def record_stderr(stderr); end
132
- def record_status(status); end
131
+ def record_commands(_commands); end
132
+
133
+ def record_stdin(_stdin); end
134
+
135
+ def record_stdout(_stdout); end
136
+
137
+ def record_stderr(_stderr); end
138
+
139
+ def record_status(_status, _allowed_status); end
133
140
  end
134
141
 
135
142
  # A default recorder. It uses the `Logger::INFO` level for normal messages and
@@ -138,16 +145,18 @@ module Cheetah
138
145
  class DefaultRecorder < Recorder
139
146
  # @private
140
147
  STREAM_INFO = {
141
- :stdin => { :name => "Standard input", :method => :info },
142
- :stdout => { :name => "Standard output", :method => :info },
143
- :stderr => { :name => "Error output", :method => :error }
144
- }
148
+ stdin: { name: "Standard input", method: :info },
149
+ stdout: { name: "Standard output", method: :info },
150
+ stderr: { name: "Error output", method: :error }
151
+ }.freeze
145
152
 
146
153
  def initialize(logger)
154
+ super()
155
+
147
156
  @logger = logger
148
157
 
149
- @stream_used = { :stdin => false, :stdout => false, :stderr => false }
150
- @stream_buffer = { :stdin => "", :stdout => "", :stderr => "" }
158
+ @stream_used = { stdin: false, stdout: false, stderr: false }
159
+ @stream_buffer = { stdin: +"", stdout: +"", stderr: +"" }
151
160
  end
152
161
 
153
162
  def record_commands(commands)
@@ -166,24 +175,25 @@ module Cheetah
166
175
  log_stream_increment(:stderr, stderr)
167
176
  end
168
177
 
169
- def record_status(status)
178
+ def record_status(status, allowed_status)
170
179
  log_stream_remainder(:stdin)
171
180
  log_stream_remainder(:stdout)
172
181
  log_stream_remainder(:stderr)
173
182
 
174
- @logger.send status.success? ? :info : :error,
175
- "Status: #{status.exitstatus}"
183
+ @logger.send allowed_status ? :info : :error,
184
+ "Status: #{status.exitstatus}"
176
185
  end
177
186
 
178
187
  protected
179
188
 
180
189
  def format_commands(commands)
181
- '"' + commands.map { |c| Shellwords.join(c) }.join(" | ") + '"'
190
+ "\"#{commands.map { |c| Shellwords.join(c) }.join(' | ')}\""
182
191
  end
183
192
 
184
193
  def log_stream_increment(stream, data)
185
194
  @stream_buffer[stream] + data =~ /\A((?:.*\n)*)(.*)\z/
186
- lines, rest = $1, $2
195
+ lines = Regexp.last_match(1)
196
+ rest = Regexp.last_match(2)
187
197
 
188
198
  lines.each_line { |l| log_stream_line(stream, l) }
189
199
 
@@ -192,9 +202,9 @@ module Cheetah
192
202
  end
193
203
 
194
204
  def log_stream_remainder(stream)
195
- if @stream_used[stream] && !@stream_buffer[stream].empty?
196
- log_stream_line(stream, @stream_buffer[stream])
197
- end
205
+ return if !@stream_used[stream] || @stream_buffer[stream].empty?
206
+
207
+ log_stream_line(stream, @stream_buffer[stream])
198
208
  end
199
209
 
200
210
  def log_stream_line(stream, line)
@@ -207,11 +217,13 @@ module Cheetah
207
217
 
208
218
  # @private
209
219
  BUILTIN_DEFAULT_OPTIONS = {
210
- :stdin => "",
211
- :stdout => nil,
212
- :stderr => nil,
213
- :logger => nil
214
- }
220
+ stdin: "",
221
+ stdout: nil,
222
+ stderr: nil,
223
+ logger: nil,
224
+ env: {},
225
+ chroot: "/"
226
+ }.freeze
215
227
 
216
228
  READ = 0 # @private
217
229
  WRITE = 1 # @private
@@ -225,7 +237,7 @@ module Cheetah
225
237
  # By default, no values are specified here.
226
238
  #
227
239
  # @example Setting a logger once for execution of multiple commands
228
- # Cheetah.default_options = { :logger = my_logger }
240
+ # Cheetah.default_options = { logger: my_logger }
229
241
  # Cheetah.run("./configure")
230
242
  # Cheetah.run("make")
231
243
  # Cheetah.run("make", "install")
@@ -244,7 +256,7 @@ module Cheetah
244
256
  # multiple command case, the execution succeeds if the last command can be
245
257
  # executed and returns a zero exit status.)
246
258
  #
247
- # Commands and their arguments never undergo shell expansion they are
259
+ # Commands and their arguments never undergo shell expansion - they are
248
260
  # passed directly to the operating system. While this may create some
249
261
  # inconvenience in certain cases, it eliminates a whole class of security
250
262
  # bugs.
@@ -296,6 +308,14 @@ module Cheetah
296
308
  # execution
297
309
  # @option options [Recorder, nil] :recorder (DefaultRecorder.new) recorder
298
310
  # to handle the command execution logging
311
+ # @option options [Fixnum, .include?, nil] :allowed_exitstatus (nil)
312
+ # Allows to specify allowed exit codes that do not cause exception. It
313
+ # adds as last element of result exitstatus.
314
+ # @option options [Hash] :env ({})
315
+ # Allows to update ENV for the time of running the command. if key maps to nil value it
316
+ # is deleted from ENV.
317
+ # @option options [String] :chroot ("/")
318
+ # Allows to run on different system root.
299
319
  #
300
320
  # @example
301
321
  # Cheetah.run("tar", "xzf", "foo.tar.gz")
@@ -325,16 +345,16 @@ module Cheetah
325
345
  # in the first variant
326
346
  #
327
347
  # @example
328
- # processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], :stdout => :capture)
348
+ # processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], stdout: :capture)
329
349
  #
330
350
  # @raise [ExecutionFailed] when the execution fails
331
351
  #
332
352
  # @example Run a command and capture its output
333
- # files = Cheetah.run("ls", "-la", :stdout => capture)
353
+ # files = Cheetah.run("ls", "-la", stdout: :capture)
334
354
  #
335
355
  # @example Run a command and capture its output into a stream
336
356
  # File.open("files.txt", "w") do |stdout|
337
- # Cheetah.run("ls", "-la", :stdout => stdout)
357
+ # Cheetah.run("ls", "-la", stdout: stdout)
338
358
  # end
339
359
  #
340
360
  # @example Run a command and handle errors
@@ -345,10 +365,35 @@ module Cheetah
345
365
  # puts "Standard output: #{e.stdout}"
346
366
  # puts "Error ouptut: #{e.stderr}"
347
367
  # end
368
+ #
369
+ # @example Run a command with expected false and handle errors
370
+ # begin
371
+ # # exit code 1 for grep mean not found
372
+ # result = Cheetah.run("grep", "userA", "/etc/passwd", allowed_exitstatus: 1)
373
+ # if result == 0
374
+ # puts "found"
375
+ # else
376
+ # puts "not found"
377
+ # end
378
+ # rescue Cheetah::ExecutionFailed => e
379
+ # puts e.message
380
+ # puts "Standard output: #{e.stdout}"
381
+ # puts "Error ouptut: #{e.stderr}"
382
+ # end
383
+ #
384
+ # @example more complex example with allowed_exitstatus
385
+ # stdout, exitcode = Cheetah.run("cmd", stdout: :capture, allowed_exitstatus: 1..5)
386
+ #
387
+
348
388
  def run(*args)
349
389
  options = args.last.is_a?(Hash) ? args.pop : {}
350
390
  options = BUILTIN_DEFAULT_OPTIONS.merge(@default_options).merge(options)
351
391
 
392
+ options[:stdin] ||= "" # allow passing nil stdin see issue gh#11
393
+ if !options[:allowed_exitstatus].respond_to?(:include?)
394
+ options[:allowed_exitstatus] = Array(options[:allowed_exitstatus])
395
+ end
396
+
352
397
  streamed = compute_streamed(options)
353
398
  streams = build_streams(options, streamed)
354
399
  commands = build_commands(args)
@@ -356,39 +401,63 @@ module Cheetah
356
401
 
357
402
  recorder.record_commands(commands)
358
403
 
359
- pid, pipes = fork_commands(commands)
404
+ pid, pipes = fork_commands(commands, options)
360
405
  select_loop(streams, pipes, recorder)
361
- pid, status = Process.wait2(pid)
406
+ _pid, status = Process.wait2(pid)
407
+
408
+ # when more exit status are allowed, then pass it below that it did
409
+ # not fail (bsc#1153749)
410
+ success = allowed_status?(status, options)
362
411
 
363
412
  begin
364
- check_errors(commands, status, streams, streamed)
413
+ report_errors(commands, status, streams, streamed) if !success
365
414
  ensure
366
- recorder.record_status(status)
415
+ # backward compatibility for recorders with just single parameter
416
+ if recorder.method(:record_status).arity == 1
417
+ recorder.record_status(status)
418
+ else
419
+ recorder.record_status(status, success)
420
+ end
367
421
  end
368
422
 
369
- build_result(streams, options)
423
+ build_result(streams, status, options)
370
424
  end
371
425
 
372
426
  private
373
427
 
428
+ def allowed_status?(status, options)
429
+ exit_status = status.exitstatus
430
+ return exit_status.zero? unless allowed_exitstatus?(options)
431
+
432
+ options[:allowed_exitstatus].include?(exit_status)
433
+ end
434
+
374
435
  # Parts of Cheetah.run
375
436
 
437
+ def with_env(env, &block)
438
+ old_env = ENV.to_hash
439
+ ENV.update(env)
440
+ block.call
441
+ ensure
442
+ ENV.replace(old_env)
443
+ end
444
+
376
445
  def compute_streamed(options)
377
446
  # The assumption for :stdout and :stderr is that anything except :capture
378
447
  # and nil is an IO-like object. We avoid detecting it directly to allow
379
448
  # passing StringIO, mocks, etc.
380
449
  {
381
- :stdin => !options[:stdin].is_a?(String),
382
- :stdout => ![nil, :capture].include?(options[:stdout]),
383
- :stderr => ![nil, :capture].include?(options[:stderr])
450
+ stdin: !options[:stdin].is_a?(String),
451
+ stdout: ![nil, :capture].include?(options[:stdout]),
452
+ stderr: ![nil, :capture].include?(options[:stderr])
384
453
  }
385
454
  end
386
455
 
387
456
  def build_streams(options, streamed)
388
457
  {
389
- :stdin => streamed[:stdin] ? options[:stdin] : StringIO.new(options[:stdin]),
390
- :stdout => streamed[:stdout] ? options[:stdout] : StringIO.new(""),
391
- :stderr => streamed[:stderr] ? options[:stderr] : StringIO.new("")
458
+ stdin: streamed[:stdin] ? options[:stdin] : StringIO.new(options[:stdin]),
459
+ stdout: streamed[:stdout] ? options[:stdout] : StringIO.new(+""),
460
+ stderr: streamed[:stderr] ? options[:stderr] : StringIO.new(+"")
392
461
  }
393
462
  end
394
463
 
@@ -410,7 +479,8 @@ module Cheetah
410
479
  # The following code ensures that the result consistently (in all three
411
480
  # cases) contains an array of arrays specifying commands and their
412
481
  # arguments.
413
- args.all? { |a| a.is_a?(Array) } ? args : [args]
482
+ commands = args.all? { |a| a.is_a?(Array) } ? args : [args]
483
+ commands.map { |c| c.map(&:to_s) }
414
484
  end
415
485
 
416
486
  def build_recorder(options)
@@ -421,54 +491,106 @@ module Cheetah
421
491
  end
422
492
  end
423
493
 
424
- def fork_commands_recursive(commands, pipes)
425
- fork do
426
- begin
427
- if commands.size == 1
428
- pipes[:stdin][WRITE].close
429
- STDIN.reopen(pipes[:stdin][READ])
430
- pipes[:stdin][READ].close
431
- else
432
- pipe_to_child = IO.pipe
433
-
434
- fork_commands_recursive(commands[0..-2], {
435
- :stdin => pipes[:stdin],
436
- :stdout => pipe_to_child,
437
- :stderr => pipes[:stderr]
438
- })
439
-
440
- pipes[:stdin][READ].close
441
- pipes[:stdin][WRITE].close
442
-
443
- pipe_to_child[WRITE].close
444
- STDIN.reopen(pipe_to_child[READ])
445
- pipe_to_child[READ].close
446
- end
494
+ # Reopen *stream* to write **into** the writing half of *pipe*
495
+ # and close the reading half of *pipe*.
496
+ # @param pipe [Array<IO>] a pair of IOs as returned from IO.pipe
497
+ # @param stream [IO]
498
+ def into_pipe(stream, pipe)
499
+ stream.reopen(pipe[WRITE])
500
+ pipe[WRITE].close
501
+ pipe[READ].close
502
+ end
503
+
504
+ # Reopen *stream* to read **from** the reading half of *pipe*
505
+ # and close the writing half of *pipe*.
506
+ # @param pipe [Array<IO>] a pair of IOs as returned from IO.pipe
507
+ # @param stream [IO]
508
+ def from_pipe(stream, pipe)
509
+ stream.reopen(pipe[READ])
510
+ pipe[READ].close
511
+ pipe[WRITE].close
512
+ end
513
+
514
+ def chroot_step(options)
515
+ return options if [nil, "/"].include?(options[:chroot])
447
516
 
448
- pipes[:stdout][READ].close
449
- STDOUT.reopen(pipes[:stdout][WRITE])
450
- pipes[:stdout][WRITE].close
517
+ options = options.dup
518
+ # delete chroot option otherwise in pipe will chroot each fork recursively
519
+ root = options.delete(:chroot)
520
+ Dir.chroot(root)
521
+ # curdir can be outside chroot which is considered as security problem
522
+ Dir.chdir("/")
451
523
 
452
- pipes[:stderr][READ].close
453
- STDERR.reopen(pipes[:stderr][WRITE])
454
- pipes[:stderr][WRITE].close
524
+ options
525
+ end
526
+
527
+ def fork_commands_recursive(commands, pipes, options)
528
+ fork do
529
+ # support chrooting
530
+ options = chroot_step(options)
531
+
532
+ if commands.size == 1
533
+ from_pipe($stdin, pipes[:stdin])
534
+ else
535
+ pipe_to_child = IO.pipe
536
+
537
+ fork_commands_recursive(commands[0..-2],
538
+ {
539
+ stdin: pipes[:stdin],
540
+ stdout: pipe_to_child,
541
+ stderr: pipes[:stderr]
542
+ },
543
+ options)
544
+
545
+ pipes[:stdin][READ].close
546
+ pipes[:stdin][WRITE].close
547
+
548
+ from_pipe($stdin, pipe_to_child)
549
+ end
455
550
 
456
- # All file descriptors from 3 above should be closed here, but since I
457
- # don't know about any way how to detect the maximum file descriptor
458
- # number portably in Ruby, I didn't implement it. Patches welcome.
551
+ into_pipe($stdout, pipes[:stdout])
552
+ into_pipe($stderr, pipes[:stderr])
459
553
 
460
- command, *args = commands.last
554
+ close_fds
555
+
556
+ command, *args = commands.last
557
+ with_env(options[:env]) do
461
558
  exec([command, command], *args)
462
- rescue SystemCallError => e
463
- exit!(127)
464
559
  end
560
+ rescue SystemCallError => e
561
+ # depends when failed, if pipe is already redirected or not, so lets find it
562
+ output = pipes[:stderr][WRITE].closed? ? $stderr : pipes[:stderr][WRITE]
563
+ output.puts e.message
564
+
565
+ exit!(127)
465
566
  end
466
567
  end
467
568
 
468
- def fork_commands(commands)
469
- pipes = { :stdin => IO.pipe, :stdout => IO.pipe, :stderr => IO.pipe }
569
+ # closes all open fds starting with 3 and above
570
+ def close_fds
571
+ # NOTE: this will work only if unix has /proc filesystem. If it does not
572
+ # have it, it won't close other fds.
573
+ Dir.glob("/proc/self/fd/*").each do |path|
574
+ fd = File.basename(path).to_i
575
+ next if (0..2).include?(fd)
470
576
 
471
- pid = fork_commands_recursive(commands, pipes)
577
+ # here we intentionally ignore some failures when fd close failed
578
+ # rubocop:disable Lint/SuppressedException
579
+ begin
580
+ IO.new(fd).close
581
+ # Ruby reserves some fds for its VM and it result in this exception
582
+ rescue ArgumentError
583
+ # Ignore if close failed with invalid FD
584
+ rescue Errno::EBADF
585
+ end
586
+ # rubocop:enable Lint/SuppressedException
587
+ end
588
+ end
589
+
590
+ def fork_commands(commands, options)
591
+ pipes = { stdin: IO.pipe, stdout: IO.pipe, stderr: IO.pipe }
592
+
593
+ pid = fork_commands_recursive(commands, pipes, options)
472
594
 
473
595
  [
474
596
  pipes[:stdin][READ],
@@ -508,11 +630,9 @@ module Cheetah
508
630
  break if pipes_readable.empty? && pipes_writable.empty?
509
631
 
510
632
  ios_read, ios_write, ios_error = select(pipes_readable, pipes_writable,
511
- pipes_readable + pipes_writable)
633
+ pipes_readable + pipes_writable)
512
634
 
513
- if !ios_error.empty?
514
- raise IOError, "Error when communicating with executed program."
515
- end
635
+ raise IOError, "Error when communicating with executed program." if !ios_error.empty?
516
636
 
517
637
  ios_read.each do |pipe|
518
638
  begin
@@ -540,46 +660,61 @@ module Cheetah
540
660
  end
541
661
  end
542
662
 
543
- def check_errors(commands, status, streams, streamed)
663
+ def report_errors(commands, status, streams, streamed)
544
664
  return if status.success?
545
665
 
546
666
  stderr_part = if streamed[:stderr]
547
- " (error output streamed away)"
548
- elsif streams[:stderr].string.empty?
549
- " (no error output)"
550
- else
551
- lines = streams[:stderr].string.split("\n")
552
- ": " + lines.first + (lines.size > 1 ? " (...)" : "")
553
- end
667
+ " (error output streamed away)"
668
+ elsif streams[:stderr].string.empty?
669
+ " (no error output)"
670
+ else
671
+ lines = streams[:stderr].string.split("\n")
672
+ ": #{lines.first}#{lines.size > 1 ? ' (...)' : ''}"
673
+ end
554
674
 
555
675
  raise ExecutionFailed.new(
556
676
  commands,
557
677
  status,
558
678
  streamed[:stdout] ? nil : streams[:stdout].string,
559
679
  streamed[:stderr] ? nil : streams[:stderr].string,
560
- "Execution of #{format_commands(commands)} " +
680
+ "Execution of #{format_commands(commands)} " \
561
681
  "failed with status #{status.exitstatus}#{stderr_part}."
562
682
  )
563
683
  end
564
684
 
565
- def build_result(streams, options)
566
- case [options[:stdout] == :capture, options[:stderr] == :capture]
567
- when [false, false]
568
- nil
569
- when [true, false]
570
- streams[:stdout].string
571
- when [false, true]
572
- streams[:stderr].string
573
- when [true, true]
574
- [streams[:stdout].string, streams[:stderr].string]
685
+ def build_result(streams, status, options)
686
+ res = case [options[:stdout] == :capture, options[:stderr] == :capture]
687
+ when [false, false]
688
+ nil
689
+ when [true, false]
690
+ streams[:stdout].string
691
+ when [false, true]
692
+ streams[:stderr].string
693
+ when [true, true]
694
+ [streams[:stdout].string, streams[:stderr].string]
695
+ end
696
+
697
+ if allowed_exitstatus?(options)
698
+ if res.nil?
699
+ res = status.exitstatus
700
+ else
701
+ res = Array(res)
702
+ res << status.exitstatus
703
+ end
575
704
  end
705
+
706
+ res
707
+ end
708
+
709
+ def allowed_exitstatus?(options)
710
+ # more exit status allowed for non array or non empty array
711
+ !options[:allowed_exitstatus].is_a?(Array) || !options[:allowed_exitstatus].empty?
576
712
  end
577
713
 
578
714
  def format_commands(commands)
579
- '"' + commands.map { |c| Shellwords.join(c) }.join(" | ") + '"'
715
+ "\"#{commands.map { |c| Shellwords.join(c) }.join(' | ')}\""
580
716
  end
581
717
  end
582
718
 
583
719
  self.default_options = {}
584
720
  end
585
-
metadata CHANGED
@@ -1,80 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cheetah
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
5
- prerelease:
4
+ version: 1.0.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - David Majda
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-11-21 00:00:00.000000000 Z
11
+ date: 2021-12-01 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: abstract_method
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ~>
17
+ - - "~>"
20
18
  - !ruby/object:Gem::Version
21
19
  version: '1.2'
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ~>
24
+ - - "~>"
28
25
  - !ruby/object:Gem::Version
29
26
  version: '1.2'
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: rspec
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - "~>"
36
32
  - !ruby/object:Gem::Version
37
- version: '0'
33
+ version: '3.3'
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - "~>"
44
39
  - !ruby/object:Gem::Version
45
- version: '0'
46
- - !ruby/object:Gem::Dependency
47
- name: redcarpet
48
- requirement: !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
51
- - - ! '>='
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ! '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
40
+ version: '3.3'
62
41
  - !ruby/object:Gem::Dependency
63
42
  name: yard
64
43
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
44
  requirements:
67
- - - ! '>='
45
+ - - ">="
68
46
  - !ruby/object:Gem::Version
69
- version: '0'
47
+ version: 0.9.11
70
48
  type: :development
71
49
  prerelease: false
72
50
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
51
  requirements:
75
- - - ! '>='
52
+ - - ">="
76
53
  - !ruby/object:Gem::Version
77
- version: '0'
54
+ version: 0.9.11
78
55
  description: Your swiss army knife for executing external commands in Ruby safely
79
56
  and conveniently.
80
57
  email: dmajda@suse.de
@@ -91,28 +68,26 @@ files:
91
68
  homepage: https://github.com/openSUSE/cheetah
92
69
  licenses:
93
70
  - MIT
71
+ metadata: {}
94
72
  post_install_message:
95
73
  rdoc_options: []
96
74
  require_paths:
97
75
  - lib
98
76
  required_ruby_version: !ruby/object:Gem::Requirement
99
- none: false
100
77
  requirements:
101
- - - ! '>='
78
+ - - ">="
102
79
  - !ruby/object:Gem::Version
103
- version: '0'
80
+ version: '2.5'
104
81
  required_rubygems_version: !ruby/object:Gem::Requirement
105
- none: false
106
82
  requirements:
107
- - - ! '>='
83
+ - - ">="
108
84
  - !ruby/object:Gem::Version
109
85
  version: '0'
110
86
  requirements: []
111
87
  rubyforge_project:
112
- rubygems_version: 1.8.23
88
+ rubygems_version: 2.7.6.3
113
89
  signing_key:
114
- specification_version: 3
90
+ specification_version: 4
115
91
  summary: Your swiss army knife for executing external commands in Ruby safely and
116
92
  conveniently.
117
93
  test_files: []
118
- has_rdoc: