shexecutor 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +13 -5
- data/lib/shexecutor/version.rb +1 -1
- data/lib/shexecutor.rb +71 -15
- data/spec/executor_spec.rb +96 -35
- data/spec/mocks/result.rb +3 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7bd1eafe5b0bcf3c40dfb7abcb428642fd8f56a
|
4
|
+
data.tar.gz: 5b24e222f42885b8e4e3e4f231d44eb623f60f09
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 562083bd0cd5db122e2332955518bb19cc403940bb98fdadc8edcaacc26724269dec003a28bfdcbff5107f1d0376357fd4e5e038ff1d806582091531253f111f
|
7
|
+
data.tar.gz: c46cd8e3d5f8835bf70bce083a02c692b75f35e87b818725e2dfee1e50104f0b66a48f7c99fdabb675f4dc54b7b43139abe55907f9293b108372b88f1dc61db2
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
# SHExecutor
|
2
2
|
|
3
|
-
SHExecutor is a convenience wrapper for executing shell commands from with-in a ruby process.
|
3
|
+
SHExecutor is a convenience wrapper for executing shell commands from with-in a ruby process.
|
4
4
|
|
5
5
|
## It supports:
|
6
6
|
- blocking indefinitely, on exit you can get status, stdout and stderr
|
7
7
|
- blocking with timeout, on exit you can get status, stdout, stderr, timeout exception (then output streams are not available)
|
8
8
|
- non-blocking, then you get no output streams, but status of the forked process
|
9
9
|
- replacement, then you get a replaced process and none of the above
|
10
|
-
|
11
|
-
## It is intended to support (future release):
|
12
10
|
- redirecting of stderr and stdout, separately, to file, with the option to overwrite or append
|
13
11
|
|
14
12
|
## It does not support:
|
15
13
|
- streaming input to stdin
|
16
|
-
- large stdout and stderr outputs that require real-time reading from the pipes
|
17
14
|
|
18
15
|
## For any executor:
|
19
16
|
- you can ask status, which will tell you "not executed", current status (e.g. run or sleep) and "no longer executing"
|
@@ -57,7 +54,7 @@ puts "For more see: #{iut.result.methods}"
|
|
57
54
|
```
|
58
55
|
iut = SHExecutor::Executor.new({:timeout => 1, :wait_for_completion => true, :application_path => "/bin/sleep", :params => ["2"]})
|
59
56
|
iut.execute
|
60
|
-
# Timeout::Error gets raised
|
57
|
+
# Timeout::Error gets raised. The spawned process continues and needs to be managed separately if required.
|
61
58
|
```
|
62
59
|
|
63
60
|
###Non-blocking
|
@@ -83,6 +80,17 @@ iut.execute
|
|
83
80
|
|
84
81
|
```
|
85
82
|
iut.flush
|
83
|
+
puts iut.stdout
|
84
|
+
puts iut.stderr
|
85
|
+
```
|
86
|
+
|
87
|
+
###redirecting stdout and stderr
|
88
|
+
```
|
89
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => "/bin/sleep", :params => ["1"], :stdout_path => "/tmp/mystdout", :stderr_path => "/tmp/mystderr", :append_stdout_path => false, :append_stderr_path => true})
|
90
|
+
iut.execute
|
91
|
+
iut.flush
|
92
|
+
puts iut.stdout
|
93
|
+
puts iut.stderr
|
86
94
|
```
|
87
95
|
|
88
96
|
Remember to call iut.flush in order to stream stdout and stderr to the Executor object.
|
data/lib/shexecutor/version.rb
CHANGED
data/lib/shexecutor.rb
CHANGED
@@ -6,7 +6,7 @@ module SHExecutor
|
|
6
6
|
:timeout => -1,
|
7
7
|
:protect_against_injection => true,
|
8
8
|
:stdout_path => nil,
|
9
|
-
:
|
9
|
+
:stderr_path => nil,
|
10
10
|
:append_stdout_path => true,
|
11
11
|
:append_stderr_path => true,
|
12
12
|
:replace => false,
|
@@ -23,6 +23,8 @@ module SHExecutor
|
|
23
23
|
attr_accessor :stderr
|
24
24
|
attr_accessor :result
|
25
25
|
attr_accessor :pid
|
26
|
+
attr_accessor :data_out
|
27
|
+
attr_accessor :data_err
|
26
28
|
|
27
29
|
def initialize(options = ::SHExecutor::default_options)
|
28
30
|
# set default options
|
@@ -47,16 +49,20 @@ module SHExecutor
|
|
47
49
|
@result.value
|
48
50
|
end
|
49
51
|
|
52
|
+
def possible_injection?(application_path)
|
53
|
+
(@options[:protect_against_injection]) and (@options[:application_path].include?(" ") or @options[:application_path].tainted?)
|
54
|
+
end
|
55
|
+
|
50
56
|
def validate
|
51
57
|
errors = []
|
52
|
-
if (!@options[:application_path].nil? and @options[:application_path].strip != "")
|
58
|
+
if (@options[:protect_against_injection]) and (!@options[:application_path].nil? and @options[:application_path].strip != "")
|
53
59
|
if (File.exists?(@options[:application_path]))
|
54
60
|
errors << "Application path not executable" if !File.executable?(@options[:application_path])
|
55
61
|
else
|
56
62
|
errors << "Application path not found"
|
57
63
|
end
|
58
64
|
|
59
|
-
errors << "Suspected injection vulnerability due to space in application_path. Turn off strict checking if you are sure" if @options[:application_path]
|
65
|
+
errors << "Suspected injection vulnerability due to space in application_path or the object being marked as 'tainted' by Ruby. Turn off strict checking if you are sure by setting :protect_against_injection to false" if possible_injection?(@options[:application_path])
|
60
66
|
|
61
67
|
else
|
62
68
|
errors << "No application path provided" if (@options[:application_path].nil?) or (@options[:application_path].strip == "")
|
@@ -88,32 +94,82 @@ module SHExecutor
|
|
88
94
|
# Check status? before calling this to see if the process has completed if you do not want to block
|
89
95
|
def flush
|
90
96
|
#store output for inspection
|
91
|
-
|
92
|
-
|
97
|
+
stdout_data = @data_out.string
|
98
|
+
stderr_data = @data_err.string
|
99
|
+
@stdout = stdout_data if stdout_data != ""
|
100
|
+
@stderr = stderr_data if stderr_data != ""
|
101
|
+
stdout_to_file if (@options[:stdout_path])
|
102
|
+
stderr_to_file if (@options[:stderr_path])
|
93
103
|
end
|
94
104
|
|
95
105
|
private
|
96
106
|
|
107
|
+
def buffer_to_file(buffer, path, append)
|
108
|
+
if not append
|
109
|
+
FileUtils.rm_f(path)
|
110
|
+
end
|
111
|
+
File.write(path, buffer, buffer.size, mode: 'a')
|
112
|
+
end
|
113
|
+
|
114
|
+
def stdout_to_file
|
115
|
+
buffer_to_file(@data_out.string, @options[:stdout_path], @options[:append_stdout_path]) if @options[:stdout_path]
|
116
|
+
end
|
117
|
+
|
118
|
+
def stderr_to_file
|
119
|
+
buffer_to_file(@data_err.string, @options[:stderr_path], @options[:append_stderr_path]) if @options[:stderr_path]
|
120
|
+
end
|
121
|
+
|
97
122
|
def replace_process
|
98
123
|
validate
|
99
124
|
@options[:params].nil? ? exec(@options[:application_path]) : exec(@options[:application_path], *@options[:params])
|
100
125
|
end
|
101
126
|
|
127
|
+
def run_process(application_path, options = "")
|
128
|
+
data_out = StringIO.new
|
129
|
+
data_err = StringIO.new
|
130
|
+
t0 = nil
|
131
|
+
Open3::popen3(application_path, options) do |stdin, stdout, stderr, thr|
|
132
|
+
t0 = thr
|
133
|
+
# read srderr and stdout into buffers to prevent blocking
|
134
|
+
t1 = Thread.new do
|
135
|
+
IO.copy_stream(stdout, data_out)
|
136
|
+
end
|
137
|
+
t2 = Thread.new do
|
138
|
+
IO.copy_stream(stderr, data_err)
|
139
|
+
end
|
140
|
+
stdin.close
|
141
|
+
#These streams should never produce IOErrors. If they do, memory is shot.
|
142
|
+
#Having them abort on exception interrupts TimeoutError, so turn abort of
|
143
|
+
#wanting to see timeouts
|
144
|
+
t1.abort_on_exception = false if should_timeout?
|
145
|
+
t2.abort_on_exception = false if should_timeout?
|
146
|
+
#No need to join here. t0 is joined by the caller.
|
147
|
+
#Explicitly join here if not configured for timeout to make the point that
|
148
|
+
#if you join here when configured for timeout, timeout will break, as the
|
149
|
+
#join will only finish when the thread finishes, i.e. the timeout exception
|
150
|
+
#will come too late
|
151
|
+
t1.join if not should_timeout?
|
152
|
+
t2.join if not should_timeout?
|
153
|
+
end
|
154
|
+
return data_out, data_err, t0
|
155
|
+
end
|
156
|
+
|
102
157
|
def block_process
|
103
158
|
validate
|
104
|
-
@
|
159
|
+
@data_out, @data_err, @result = run_process(@options[:application_path], *@options[:params])
|
160
|
+
@result.join
|
105
161
|
@result.value
|
106
162
|
end
|
107
163
|
|
108
164
|
def block_process_with_timeout
|
109
165
|
validate
|
110
|
-
@stdin_stream, @stdout_stream, @stderr_stream, @result = Open3::popen3(@options[:application_path], *@options[:params])
|
111
166
|
begin
|
112
167
|
Timeout.timeout(@options[:timeout]) do
|
113
|
-
@result
|
168
|
+
@data_out, @data_err, @result = run_process(@options[:application_path], *@options[:params])
|
114
169
|
end
|
170
|
+
@result.join
|
171
|
+
@result.value
|
115
172
|
rescue Timeout::Error => ex
|
116
|
-
Process.kill 9, @result.pid
|
117
173
|
raise ex
|
118
174
|
end
|
119
175
|
end
|
@@ -122,11 +178,11 @@ module SHExecutor
|
|
122
178
|
validate
|
123
179
|
@stdin_stream, @stdout_stream, @stderr_stream, @result = Open3::popen3(@options[:application_path], *@options[:params])
|
124
180
|
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def should_timeout?
|
185
|
+
@options[:timeout] > 0
|
186
|
+
end
|
125
187
|
end
|
126
188
|
end
|
127
|
-
|
128
|
-
#def redirect_all_output_to_file file
|
129
|
-
# log = File.new(file, "w+")
|
130
|
-
# STDOUT.reopen log; STDERR.reopen log
|
131
|
-
# STDOUT.sync = true
|
132
|
-
#end
|
data/spec/executor_spec.rb
CHANGED
@@ -23,7 +23,7 @@ describe 'Executor' do
|
|
23
23
|
:timeout => -1,
|
24
24
|
:protect_against_injection=>true,
|
25
25
|
:stdout_path => '/log/testlog',
|
26
|
-
:
|
26
|
+
:stderr_path => '/log/testlog',
|
27
27
|
:append_stdout_path => false,
|
28
28
|
:append_stderr_path => false,
|
29
29
|
:replace => false,
|
@@ -64,13 +64,13 @@ describe 'Executor' do
|
|
64
64
|
iut = SHExecutor::Executor.new({:application_path => f.path})
|
65
65
|
expect{
|
66
66
|
iut.validate
|
67
|
-
}.to raise_error(ArgumentError, "Application path not executable")
|
67
|
+
}.to raise_error(ArgumentError, "Application path not executable,Suspected injection vulnerability due to space in application_path or the object being marked as 'tainted' by Ruby. Turn off strict checking if you are sure by setting :protect_against_injection to false")
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
71
|
context 'when initialized with valid options' do
|
72
72
|
it 'validate should not raise an exception' do
|
73
|
-
iut = SHExecutor::Executor.new({:application_path => @executable_file.path})
|
73
|
+
iut = SHExecutor::Executor.new({:application_path => @executable_file.path, :protect_against_injection => false})
|
74
74
|
iut.validate
|
75
75
|
end
|
76
76
|
end
|
@@ -99,7 +99,7 @@ describe 'Executor' do
|
|
99
99
|
expect(Kernel::last_params).to eq(*test_params)
|
100
100
|
end
|
101
101
|
|
102
|
-
it 'should use exec with the command if no parameters are specified' do
|
102
|
+
it 'should use exec with the command if no parameters are specified and replacing' do
|
103
103
|
test_command = "/bin/ls"
|
104
104
|
iut = SHExecutor::Executor.new({:replace => true, :application_path => test_command})
|
105
105
|
iut.execute
|
@@ -107,6 +107,12 @@ describe 'Executor' do
|
|
107
107
|
expect(Kernel::last_params).to be_nil
|
108
108
|
end
|
109
109
|
|
110
|
+
it 'should use exec with the command if no parameters are specified and not waiting for completion' do
|
111
|
+
test_command = "/bin/ls"
|
112
|
+
iut = SHExecutor::Executor.new({:application_path => test_command, :wait_for_completion => true})
|
113
|
+
iut.execute
|
114
|
+
end
|
115
|
+
|
110
116
|
it 'should validate' do
|
111
117
|
iut = SHExecutor::Executor.new({:replace => true})
|
112
118
|
expect{
|
@@ -201,42 +207,32 @@ describe 'Executor' do
|
|
201
207
|
end
|
202
208
|
|
203
209
|
context 'when asked to execute and block' do
|
204
|
-
it 'should raise a TimeoutException if a timeout is specified and the
|
210
|
+
it 'should raise a TimeoutException if a timeout is specified and the process does not exit before' do
|
205
211
|
test_command = "/bin/sleep"
|
206
|
-
test_params = ["
|
212
|
+
test_params = ["5"]
|
207
213
|
iut = SHExecutor::Executor.new({:timeout => 1, :wait_for_completion => true, :application_path => test_command, :params => test_params})
|
208
214
|
before = Time.now
|
209
215
|
expect {
|
210
216
|
iut.execute
|
211
217
|
}.to raise_error(Timeout::Error, "execution expired")
|
212
218
|
after = Time.now
|
213
|
-
expect(after - before).to be < 2
|
214
|
-
end
|
215
|
-
|
216
|
-
it 'should kill the subprocess when a TimeoutException is raised' do
|
217
|
-
test_command = "/bin/sleep"
|
218
|
-
test_params = ["2"]
|
219
|
-
iut = SHExecutor::Executor.new({:timeout => 1, :wait_for_completion => true, :application_path => test_command, :params => test_params})
|
220
|
-
expect(Process).to receive(:kill)
|
221
|
-
expect {
|
222
|
-
iut.execute
|
223
|
-
}.to raise_error(Timeout::Error, "execution expired")
|
219
|
+
expect(after - before).to be < 2.1
|
224
220
|
end
|
225
221
|
|
226
|
-
it 'should
|
222
|
+
it 'should call run_process with the command and parameters specified' do
|
227
223
|
test_command = "/bin/ls"
|
228
224
|
test_params = ["/tmp/"]
|
229
225
|
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command, :params => test_params})
|
230
226
|
stdin = stdout = stderr = StringIO.new
|
231
|
-
expect(
|
227
|
+
expect(iut).to receive(:run_process).with(test_command, *test_params).and_return([stdout, stderr, Result.new(true)])
|
232
228
|
iut.execute
|
233
229
|
end
|
234
230
|
|
235
|
-
it 'should use
|
231
|
+
it 'should use run_process with the command' do
|
236
232
|
test_command = "/bin/ls"
|
237
233
|
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command})
|
238
234
|
stdin = stdout = stderr = StringIO.new
|
239
|
-
expect(
|
235
|
+
expect(iut).to receive(:run_process).with(test_command).and_return([stdout, stderr, Result.new(true)])
|
240
236
|
iut.execute
|
241
237
|
end
|
242
238
|
|
@@ -250,6 +246,21 @@ describe 'Executor' do
|
|
250
246
|
expect(after - before).to be > 2
|
251
247
|
end
|
252
248
|
|
249
|
+
it 'should block until completion and still have access to the ouput' do
|
250
|
+
file = Tempfile.new("testingargumenterror")
|
251
|
+
File.chmod(0744, file.path)
|
252
|
+
`echo "sleep 2" >> #{file.path}`
|
253
|
+
`echo "echo 'this did run'" >> #{file.path}`
|
254
|
+
test_command = file.path
|
255
|
+
iut = SHExecutor::Executor.new({:protect_against_injection => false, :wait_for_completion => true, :application_path => test_command})
|
256
|
+
before = Time.now
|
257
|
+
iut.execute
|
258
|
+
iut.flush
|
259
|
+
expect(iut.stdout).to eq("this did run\n")
|
260
|
+
after = Time.now
|
261
|
+
expect(after - before).to be > 2
|
262
|
+
end
|
263
|
+
|
253
264
|
it 'should validate' do
|
254
265
|
iut = SHExecutor::Executor.new({:wait_for_completion => true})
|
255
266
|
expect{
|
@@ -259,27 +270,77 @@ describe 'Executor' do
|
|
259
270
|
end
|
260
271
|
|
261
272
|
context 'when asked to redirect stdout to a file appending' do
|
262
|
-
|
263
|
-
|
273
|
+
before :each do
|
274
|
+
@stdout_test_path = '/tmp/append_stdout_path'
|
275
|
+
FileUtils.rm_f(@stdout_test_path)
|
276
|
+
end
|
264
277
|
|
265
|
-
|
266
|
-
|
267
|
-
|
278
|
+
def execute_stdout_test_command(append = true)
|
279
|
+
test_command = "/bin/echo"
|
280
|
+
test_params = ["hello world"]
|
281
|
+
@iut = SHExecutor::Executor.new({:append_stdout_path => append, :stdout_path => @stdout_test_path, :wait_for_completion => true, :application_path => test_command, :params => test_params})
|
282
|
+
@iut.execute
|
283
|
+
@iut.flush
|
284
|
+
File.open(@stdout_test_path).read
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'should append stdout to the file specified, and create it if it does not exist' do
|
288
|
+
expect(execute_stdout_test_command).to eq("hello world\n")
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'should append stdout to the file specified, if it exists' do
|
292
|
+
`echo "line 1" >> #{@stdout_test_path}`
|
293
|
+
expect(execute_stdout_test_command).to eq("line 1\nhello world\n")
|
294
|
+
end
|
268
295
|
|
269
|
-
|
270
|
-
|
296
|
+
it 'should delete if exists and create the specified file, then write to it if append is not set' do
|
297
|
+
`echo "line 1" >> #{@stdout_test_path}`
|
298
|
+
expect(execute_stdout_test_command(false)).to eq("hello world\n")
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'should raise an exception if one of the file operations fails' do
|
302
|
+
@stdout_test_path = "/tmp/thisdirectorydoesnotexistdshlgh58iyg89rlehg8y/gn.dfllgyls54gh57479gh"
|
303
|
+
expect {
|
304
|
+
execute_stdout_test_command
|
305
|
+
}.to raise_error(Errno::ENOENT, "No such file or directory - /tmp/thisdirectorydoesnotexistdshlgh58iyg89rlehg8y/gn.dfllgyls54gh57479gh")
|
306
|
+
end
|
271
307
|
end
|
272
308
|
|
273
309
|
context 'when asked to redirect stderr to a file appending' do
|
274
|
-
|
275
|
-
|
310
|
+
before :each do
|
311
|
+
@stderr_test_path = '/tmp/append_stderr_path'
|
312
|
+
FileUtils.rm_f(@stderr_test_path)
|
313
|
+
end
|
276
314
|
|
277
|
-
|
278
|
-
|
279
|
-
|
315
|
+
def execute_stderr_test_command(append = true)
|
316
|
+
test_command = "/bin/ls"
|
317
|
+
test_params = ["/tmp/thisfiledoesnotexistsatgup80wh0hgoefhgohuo4whg4whg4w5hg0"]
|
318
|
+
@iut = SHExecutor::Executor.new({:append_stderr_path => append, :stderr_path => @stderr_test_path, :wait_for_completion => true, :application_path => test_command, :params => test_params})
|
319
|
+
@iut.execute
|
320
|
+
@iut.flush
|
321
|
+
File.open(@stderr_test_path).read
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'should append stderr to the file specified, and create it if it does not exist' do
|
325
|
+
expect(execute_stderr_test_command).to eq("ls: /tmp/thisfiledoesnotexistsatgup80wh0hgoefhgohuo4whg4whg4w5hg0: No such file or directory\n")
|
326
|
+
end
|
280
327
|
|
281
|
-
|
282
|
-
|
328
|
+
it 'should append stderr to the file specified, if it exists' do
|
329
|
+
`echo "line 1" >> #{@stderr_test_path}`
|
330
|
+
expect(execute_stderr_test_command).to eq("line 1\nls: /tmp/thisfiledoesnotexistsatgup80wh0hgoefhgohuo4whg4whg4w5hg0: No such file or directory\n")
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'should delete if exists and create the specified file, then write to it if append is not set' do
|
334
|
+
`echo "line 1" >> #{@stderr_test_path}`
|
335
|
+
expect(execute_stderr_test_command(false)).to eq("ls: /tmp/thisfiledoesnotexistsatgup80wh0hgoefhgohuo4whg4whg4w5hg0: No such file or directory\n")
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'should raise an exception if one of the file operations fails' do
|
339
|
+
@stderr_test_path = "/tmp/thisdirectorydoesnotexistdshlgh58iyg89rlehg8y/gn.dfllgyls54gh57479gh"
|
340
|
+
expect {
|
341
|
+
execute_stderr_test_command
|
342
|
+
}.to raise_error(Errno::ENOENT, "No such file or directory - /tmp/thisdirectorydoesnotexistdshlgh58iyg89rlehg8y/gn.dfllgyls54gh57479gh")
|
343
|
+
end
|
283
344
|
end
|
284
345
|
|
285
346
|
context 'when asked to flush' do
|
data/spec/mocks/result.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shexecutor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ernst van Graan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|