cheetah 0.4.0 → 1.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG +37 -2
- data/README.md +94 -14
- data/VERSION +1 -1
- data/lib/cheetah/version.rb +4 -1
- data/lib/cheetah.rb +242 -107
- 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
|
8
|
-
* Added support
|
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
|
+
[](https://travis-ci.org/openSUSE/cheetah)
|
4
|
+
[](https://codeclimate.com/github/openSUSE/cheetah)
|
5
|
+
[](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", :
|
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", :
|
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
|
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", :
|
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", :
|
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", :
|
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", ".", :
|
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", :
|
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"], :
|
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
|
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
|
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", :
|
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
|
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
|
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.
|
1
|
+
1.0.0
|
data/lib/cheetah/version.rb
CHANGED
@@ -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__)
|
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__)
|
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", :
|
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", :
|
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(
|
129
|
-
|
130
|
-
def
|
131
|
-
|
132
|
-
def
|
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
|
-
:
|
142
|
-
:
|
143
|
-
:
|
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 = { :
|
150
|
-
@stream_buffer = { :
|
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
|
175
|
-
|
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
|
-
|
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
|
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
|
196
|
-
|
197
|
-
|
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
|
-
:
|
211
|
-
:
|
212
|
-
:
|
213
|
-
:
|
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 = { :
|
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
|
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"], :
|
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", :
|
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", :
|
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
|
-
|
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
|
-
|
413
|
+
report_errors(commands, status, streams, streamed) if !success
|
365
414
|
ensure
|
366
|
-
|
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
|
-
:
|
382
|
-
:
|
383
|
-
:
|
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
|
-
:
|
390
|
-
:
|
391
|
-
:
|
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
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
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
|
-
|
449
|
-
|
450
|
-
|
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
|
-
|
453
|
-
|
454
|
-
|
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
|
-
|
457
|
-
|
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
|
-
|
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
|
-
|
469
|
-
|
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
|
-
|
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
|
-
|
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
|
663
|
+
def report_errors(commands, status, streams, streamed)
|
544
664
|
return if status.success?
|
545
665
|
|
546
666
|
stderr_part = if streamed[:stderr]
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
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
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
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
|
-
|
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.
|
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:
|
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: '
|
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: '
|
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:
|
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:
|
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: '
|
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:
|
88
|
+
rubygems_version: 2.7.6.3
|
113
89
|
signing_key:
|
114
|
-
specification_version:
|
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:
|