shexecutor 0.0.6 → 0.0.7
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 +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
|