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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 610b8828b18a5c04454d349966788f26997f6f12
4
- data.tar.gz: f9e6d63577e7acd0fb79001863c6f67a4f901e0e
3
+ metadata.gz: b7bd1eafe5b0bcf3c40dfb7abcb428642fd8f56a
4
+ data.tar.gz: 5b24e222f42885b8e4e3e4f231d44eb623f60f09
5
5
  SHA512:
6
- metadata.gz: b91c99fd12ce80faaf82636b2bb4f4b055ca803674eb5f6298442d7d7bb5f6462719a42c085a3a273f66198c8d9e5ef14ce5ece0586dda73b96dc7f67c32216b
7
- data.tar.gz: ce988ff050c5e011268683556073f43c8b2ff55d393052675a3e509d734d03cbea3a141818a18389d1ee070ec4579c48a3fb7ad6bd0077f2d0418580f694d2e7
6
+ metadata.gz: 562083bd0cd5db122e2332955518bb19cc403940bb98fdadc8edcaacc26724269dec003a28bfdcbff5107f1d0376357fd4e5e038ff1d806582091531253f111f
7
+ data.tar.gz: c46cd8e3d5f8835bf70bce083a02c692b75f35e87b818725e2dfee1e50104f0b66a48f7c99fdabb675f4dc54b7b43139abe55907f9293b108372b88f1dc61db2
data/.gitignore CHANGED
@@ -2,3 +2,5 @@
2
2
  coverage
3
3
  *gem
4
4
  doc
5
+ .~lock*
6
+ Reviews.odt
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shexecutor (0.0.6)
4
+ shexecutor (0.0.7)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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. It is intended to be used for the simple execution of shell commands or scripts that are not expected to generate GBs of output or require feeding via stdin.
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 and the spawned process is killed
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.
@@ -1,3 +1,3 @@
1
1
  module SHExecutor
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
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
- :strerr_path => nil,
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].include?(" ")
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
- @stdout = @stdout_stream.gets(nil)
92
- @stderr = @stderr_stream.gets(nil)
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
- @stdin_stream, @stdout_stream, @stderr_stream, @result = Open3::popen3(@options[:application_path], *@options[:params])
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.value
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
@@ -23,7 +23,7 @@ describe 'Executor' do
23
23
  :timeout => -1,
24
24
  :protect_against_injection=>true,
25
25
  :stdout_path => '/log/testlog',
26
- :strerr_path => '/log/testlog',
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 forked process does not exit before' do
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 = ["2"]
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 use Open3::popen3 with the command and parameters specified' do
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(Open3).to receive(:popen3).with(test_command, *test_params).and_return([stdin, stdout, stderr, Result.new(true)])
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 Open3::popen3 with the command' do
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(Open3).to receive(:popen3).with(test_command).and_return([stdin, stdout, stderr, Result.new(true)])
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
- it 'should append stdout to the file specified, and create it if it does not exist'
263
- end
273
+ before :each do
274
+ @stdout_test_path = '/tmp/append_stdout_path'
275
+ FileUtils.rm_f(@stdout_test_path)
276
+ end
264
277
 
265
- context 'when asked to redirect stdout to a file overwriting' do
266
- it 'should delete if exists and create the specified file, then append stdout to it'
267
- end
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
- context 'when asked to redirect stdout' do
270
- it 'should raise an exception if one of the file operations fails'
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
- it 'should append stderr to the file specified, and create it if it does not exist'
275
- end
310
+ before :each do
311
+ @stderr_test_path = '/tmp/append_stderr_path'
312
+ FileUtils.rm_f(@stderr_test_path)
313
+ end
276
314
 
277
- context 'when asked to redirect stderr to a file overwriting' do
278
- it 'should delete if exists and create the specified file, then append stderr to it'
279
- end
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
- context 'when asked to redirect stderr' do
282
- it 'should raise an exception if one of the file operations fails'
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
@@ -4,4 +4,7 @@ class Result
4
4
  def initialize(value)
5
5
  @value = value
6
6
  end
7
+
8
+ def join
9
+ end
7
10
  end
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.6
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-15 00:00:00.000000000 Z
11
+ date: 2015-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler