shexecutor 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +47 -0
- data/LICENSE.txt +22 -0
- data/README.md +102 -0
- data/Rakefile +2 -0
- data/lib/shexecutor/version.rb +3 -0
- data/lib/shexecutor.rb +132 -0
- data/shexecutor.gemspec +27 -0
- data/spec/executor_spec.rb +306 -0
- data/spec/mocks/kernel.rb +22 -0
- data/spec/mocks/result.rb +7 -0
- data/spec/spec_helper.rb +31 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 610b8828b18a5c04454d349966788f26997f6f12
|
4
|
+
data.tar.gz: f9e6d63577e7acd0fb79001863c6f67a4f901e0e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b91c99fd12ce80faaf82636b2bb4f4b055ca803674eb5f6298442d7d7bb5f6462719a42c085a3a273f66198c8d9e5ef14ce5ece0586dda73b96dc7f67c32216b
|
7
|
+
data.tar.gz: ce988ff050c5e011268683556073f43c8b2ff55d393052675a3e509d734d03cbea3a141818a18389d1ee070ec4579c48a3fb7ad6bd0077f2d0418580f694d2e7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
shexecutor (0.0.6)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (4.0.5)
|
10
|
+
columnize (= 0.9.0)
|
11
|
+
columnize (0.9.0)
|
12
|
+
diff-lcs (1.2.5)
|
13
|
+
docile (1.1.5)
|
14
|
+
json (1.8.2)
|
15
|
+
rake (10.4.2)
|
16
|
+
rspec (3.2.0)
|
17
|
+
rspec-core (~> 3.2.0)
|
18
|
+
rspec-expectations (~> 3.2.0)
|
19
|
+
rspec-mocks (~> 3.2.0)
|
20
|
+
rspec-core (3.2.3)
|
21
|
+
rspec-support (~> 3.2.0)
|
22
|
+
rspec-expectations (3.2.1)
|
23
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
24
|
+
rspec-support (~> 3.2.0)
|
25
|
+
rspec-mocks (3.2.1)
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
27
|
+
rspec-support (~> 3.2.0)
|
28
|
+
rspec-support (3.2.2)
|
29
|
+
simplecov (0.10.0)
|
30
|
+
docile (~> 1.1.0)
|
31
|
+
json (~> 1.8)
|
32
|
+
simplecov-html (~> 0.10.0)
|
33
|
+
simplecov-html (0.10.0)
|
34
|
+
simplecov-rcov (0.2.3)
|
35
|
+
simplecov (>= 0.4.1)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
ruby
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
bundler
|
42
|
+
byebug
|
43
|
+
rake
|
44
|
+
rspec
|
45
|
+
shexecutor!
|
46
|
+
simplecov
|
47
|
+
simplecov-rcov
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Ernst van Graan
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# SHExecutor
|
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.
|
4
|
+
|
5
|
+
## It supports:
|
6
|
+
- blocking indefinitely, on exit you can get status, stdout and stderr
|
7
|
+
- blocking with timeout, on exit you can get status, stdout, stderr, timeout exception (then output streams are not available)
|
8
|
+
- non-blocking, then you get no output streams, but status of the forked process
|
9
|
+
- replacement, then you get a replaced process and none of the above
|
10
|
+
|
11
|
+
## It is intended to support (future release):
|
12
|
+
- redirecting of stderr and stdout, separately, to file, with the option to overwrite or append
|
13
|
+
|
14
|
+
## It does not support:
|
15
|
+
- streaming input to stdin
|
16
|
+
- large stdout and stderr outputs that require real-time reading from the pipes
|
17
|
+
|
18
|
+
## For any executor:
|
19
|
+
- you can ask status, which will tell you "not executed", current status (e.g. run or sleep) and "no longer executing"
|
20
|
+
- you can ask result, which gives you nil unless the process is no longer executing, in which case you get status (e.g. pid, exit code, sigints.)
|
21
|
+
|
22
|
+
For a full description of status, please see Process::Status
|
23
|
+
|
24
|
+
This gem is sponsored by Hetzner (Pty) Ltd - http://hetzner.co.za
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
Add this line to your application's Gemfile:
|
29
|
+
|
30
|
+
gem 'shexecutor'
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install shexecutor
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
###Blocking
|
43
|
+
|
44
|
+
```
|
45
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => "/bin/echo", :params => ["hello world"]})
|
46
|
+
iut.execute
|
47
|
+
iut.flush
|
48
|
+
puts "After execution status is: #{iut.status}"
|
49
|
+
# "no longer executing"
|
50
|
+
puts "out: #{iut.stdout} err: #{iut.stderr}"
|
51
|
+
puts "#{iut.result.pid} success? #{iut.result.success?} with code #{iut.result.exitstatus}"
|
52
|
+
puts "For more see: #{iut.result.methods}"
|
53
|
+
```
|
54
|
+
|
55
|
+
###Blocking with timeout
|
56
|
+
|
57
|
+
```
|
58
|
+
iut = SHExecutor::Executor.new({:timeout => 1, :wait_for_completion => true, :application_path => "/bin/sleep", :params => ["2"]})
|
59
|
+
iut.execute
|
60
|
+
# Timeout::Error gets raised and the spawned process is killed
|
61
|
+
```
|
62
|
+
|
63
|
+
###Non-blocking
|
64
|
+
|
65
|
+
```
|
66
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => "/bin/sleep", :params => ["1"]})
|
67
|
+
iut.execute
|
68
|
+
puts "Status: #{iut.status}"
|
69
|
+
# "run" or "sleep"
|
70
|
+
sleep 2
|
71
|
+
puts "Status: #{iut.status}"
|
72
|
+
# "no longer executing"
|
73
|
+
```
|
74
|
+
|
75
|
+
###Replacing
|
76
|
+
|
77
|
+
```
|
78
|
+
iut = SHExecutor::Executor.new({:replace => true, :application_path => "/bin/echo", :params => ["Your process has been assimilated"]})
|
79
|
+
iut.execute
|
80
|
+
```
|
81
|
+
|
82
|
+
###stdout and stderr
|
83
|
+
|
84
|
+
```
|
85
|
+
iut.flush
|
86
|
+
```
|
87
|
+
|
88
|
+
Remember to call iut.flush in order to stream stdout and stderr to the Executor object.
|
89
|
+
|
90
|
+
Note: calling flush will block if execute was called non-blocking, so wait until the execute has finished to preserve non-blocking behaviour.
|
91
|
+
|
92
|
+
Please send feedback and comments to the author at:
|
93
|
+
|
94
|
+
Ernst van Graan <ernst.van.graan@hetzner.co.za>
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
1. Fork it ( https://github.com/[my-github-username]/executor/fork )
|
99
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
100
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
101
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
102
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/lib/shexecutor.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require "shexecutor/version"
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
module SHExecutor
|
5
|
+
@@default_options = {
|
6
|
+
:timeout => -1,
|
7
|
+
:protect_against_injection => true,
|
8
|
+
:stdout_path => nil,
|
9
|
+
:strerr_path => nil,
|
10
|
+
:append_stdout_path => true,
|
11
|
+
:append_stderr_path => true,
|
12
|
+
:replace => false,
|
13
|
+
:wait_for_completion => false
|
14
|
+
}
|
15
|
+
|
16
|
+
def self.default_options
|
17
|
+
@@default_options
|
18
|
+
end
|
19
|
+
|
20
|
+
class Executor
|
21
|
+
attr_accessor :options
|
22
|
+
attr_accessor :stdout
|
23
|
+
attr_accessor :stderr
|
24
|
+
attr_accessor :result
|
25
|
+
attr_accessor :pid
|
26
|
+
|
27
|
+
def initialize(options = ::SHExecutor::default_options)
|
28
|
+
# set default options
|
29
|
+
@options = ::SHExecutor::default_options.dup
|
30
|
+
|
31
|
+
# then apply specified options
|
32
|
+
options.each do |key, value|
|
33
|
+
@options[key] = value
|
34
|
+
end
|
35
|
+
|
36
|
+
@options
|
37
|
+
end
|
38
|
+
|
39
|
+
def status
|
40
|
+
return "not executed" if @result.nil?
|
41
|
+
return @result.status if @result.alive?
|
42
|
+
return "no longer executing" if not @result.alive?
|
43
|
+
end
|
44
|
+
|
45
|
+
def result
|
46
|
+
return nil if status != "no longer executing"
|
47
|
+
@result.value
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate
|
51
|
+
errors = []
|
52
|
+
if (!@options[:application_path].nil? and @options[:application_path].strip != "")
|
53
|
+
if (File.exists?(@options[:application_path]))
|
54
|
+
errors << "Application path not executable" if !File.executable?(@options[:application_path])
|
55
|
+
else
|
56
|
+
errors << "Application path not found"
|
57
|
+
end
|
58
|
+
|
59
|
+
errors << "Suspected injection vulnerability due to space in application_path. Turn off strict checking if you are sure" if @options[:application_path].include?(" ")
|
60
|
+
|
61
|
+
else
|
62
|
+
errors << "No application path provided" if (@options[:application_path].nil?) or (@options[:application_path].strip == "")
|
63
|
+
end
|
64
|
+
|
65
|
+
raise ArgumentError.new(errors.join(',')) if errors.count > 0
|
66
|
+
end
|
67
|
+
|
68
|
+
def execute
|
69
|
+
@pid = nil
|
70
|
+
@stdout = nil
|
71
|
+
@stderr = nil
|
72
|
+
if (@options[:replace] == true)
|
73
|
+
replace_process
|
74
|
+
else
|
75
|
+
if (@options[:wait_for_completion])
|
76
|
+
if (@options[:timeout] <= 0)
|
77
|
+
block_process
|
78
|
+
else
|
79
|
+
block_process_with_timeout
|
80
|
+
end
|
81
|
+
else
|
82
|
+
fork_process
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Warning! This method will block if you are not blocking!
|
88
|
+
# Check status? before calling this to see if the process has completed if you do not want to block
|
89
|
+
def flush
|
90
|
+
#store output for inspection
|
91
|
+
@stdout = @stdout_stream.gets(nil)
|
92
|
+
@stderr = @stderr_stream.gets(nil)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def replace_process
|
98
|
+
validate
|
99
|
+
@options[:params].nil? ? exec(@options[:application_path]) : exec(@options[:application_path], *@options[:params])
|
100
|
+
end
|
101
|
+
|
102
|
+
def block_process
|
103
|
+
validate
|
104
|
+
@stdin_stream, @stdout_stream, @stderr_stream, @result = Open3::popen3(@options[:application_path], *@options[:params])
|
105
|
+
@result.value
|
106
|
+
end
|
107
|
+
|
108
|
+
def block_process_with_timeout
|
109
|
+
validate
|
110
|
+
@stdin_stream, @stdout_stream, @stderr_stream, @result = Open3::popen3(@options[:application_path], *@options[:params])
|
111
|
+
begin
|
112
|
+
Timeout.timeout(@options[:timeout]) do
|
113
|
+
@result.value
|
114
|
+
end
|
115
|
+
rescue Timeout::Error => ex
|
116
|
+
Process.kill 9, @result.pid
|
117
|
+
raise ex
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def fork_process
|
122
|
+
validate
|
123
|
+
@stdin_stream, @stdout_stream, @stderr_stream, @result = Open3::popen3(@options[:application_path], *@options[:params])
|
124
|
+
end
|
125
|
+
end
|
126
|
+
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/shexecutor.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'shexecutor/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "shexecutor"
|
8
|
+
spec.version = SHExecutor::VERSION
|
9
|
+
spec.authors = ["Ernst van Graan"]
|
10
|
+
spec.email = ["ernst.van.graan@hetzner.co.za"]
|
11
|
+
spec.summary = %q{Execute shell commands easily and securely}
|
12
|
+
spec.description = %q{Implements process replacement, forking, protection from shell injection, and a variety of output options}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_development_dependency 'simplecov'
|
25
|
+
spec.add_development_dependency 'simplecov-rcov'
|
26
|
+
spec.add_development_dependency 'byebug'
|
27
|
+
end
|
@@ -0,0 +1,306 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'shexecutor.rb'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'mocks/kernel.rb'
|
5
|
+
require 'mocks/result.rb'
|
6
|
+
|
7
|
+
describe 'Executor' do
|
8
|
+
before :all do
|
9
|
+
@executable_file = Tempfile.new("testingargumenterror")
|
10
|
+
File.chmod(0744, @executable_file.path)
|
11
|
+
`echo "ls" >> #{@executable_file.path}`
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when initialized with no options' do
|
15
|
+
it 'should set default options' do
|
16
|
+
@iut = SHExecutor::Executor.new
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when initialized with options' do
|
21
|
+
it 'should remember the options' do
|
22
|
+
iut_options = {
|
23
|
+
:timeout => -1,
|
24
|
+
:protect_against_injection=>true,
|
25
|
+
:stdout_path => '/log/testlog',
|
26
|
+
:strerr_path => '/log/testlog',
|
27
|
+
:append_stdout_path => false,
|
28
|
+
:append_stderr_path => false,
|
29
|
+
:replace => false,
|
30
|
+
:wait_for_completion => true
|
31
|
+
}
|
32
|
+
@iut = SHExecutor::Executor.new(iut_options)
|
33
|
+
expect(@iut.options).to eq(iut_options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when initialized with some but not all options' do
|
38
|
+
it 'should remember the options provided and default the rest' do
|
39
|
+
expected_options = ::SHExecutor::default_options
|
40
|
+
expected_options[:wait_for_completion] = false
|
41
|
+
iut_options = {
|
42
|
+
:wait_for_completion => false
|
43
|
+
}
|
44
|
+
@iut = SHExecutor::Executor.new(iut_options)
|
45
|
+
expect(@iut.options).to eq(expected_options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when initialized with one or more invalid options' do
|
50
|
+
it 'validate should raise an ArgumentError' do
|
51
|
+
iut = SHExecutor::Executor.new({:application_path => nil})
|
52
|
+
expect{
|
53
|
+
iut.validate
|
54
|
+
}.to raise_error(ArgumentError, "No application path provided")
|
55
|
+
iut = SHExecutor::Executor.new({:application_path => ""})
|
56
|
+
expect{
|
57
|
+
iut.validate
|
58
|
+
}.to raise_error(ArgumentError, "No application path provided")
|
59
|
+
iut = SHExecutor::Executor.new({:application_path => "/kjsdfhgjkgsjk"})
|
60
|
+
expect{
|
61
|
+
iut.validate
|
62
|
+
}.to raise_error(ArgumentError, "Application path not found")
|
63
|
+
f = Tempfile.new("testingargumenterror")
|
64
|
+
iut = SHExecutor::Executor.new({:application_path => f.path})
|
65
|
+
expect{
|
66
|
+
iut.validate
|
67
|
+
}.to raise_error(ArgumentError, "Application path not executable")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when initialized with valid options' do
|
72
|
+
it 'validate should not raise an exception' do
|
73
|
+
iut = SHExecutor::Executor.new({:application_path => @executable_file.path})
|
74
|
+
iut.validate
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when asked to execute" do
|
79
|
+
it 'should clear stderr and stdout before execution' do
|
80
|
+
test_command = "/bin/echo"
|
81
|
+
test_params = ["Hello"]
|
82
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command, :params => test_params})
|
83
|
+
iut.stdout = "stdout before"
|
84
|
+
iut.stderr = "error before"
|
85
|
+
iut.execute
|
86
|
+
iut.flush
|
87
|
+
expect(iut.stderr).to be_nil
|
88
|
+
expect(iut.stdout).to eq("Hello\n")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when asked to execute, replacing the current process' do
|
93
|
+
it 'should use exec with the command and parameters specified' do
|
94
|
+
test_command = "/bin/ls"
|
95
|
+
test_params = ["/tmp/"]
|
96
|
+
iut = SHExecutor::Executor.new({:replace => true, :application_path => test_command, :params => test_params})
|
97
|
+
iut.execute
|
98
|
+
expect(Kernel::last_command).to eq(test_command)
|
99
|
+
expect(Kernel::last_params).to eq(*test_params)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should use exec with the command if no parameters are specified' do
|
103
|
+
test_command = "/bin/ls"
|
104
|
+
iut = SHExecutor::Executor.new({:replace => true, :application_path => test_command})
|
105
|
+
iut.execute
|
106
|
+
expect(Kernel::last_command).to eq(test_command)
|
107
|
+
expect(Kernel::last_params).to be_nil
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should validate' do
|
111
|
+
iut = SHExecutor::Executor.new({:replace => true})
|
112
|
+
expect{
|
113
|
+
iut.execute
|
114
|
+
}.to raise_error(ArgumentError, "No application path provided")
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should not replace if replace is not true' do
|
118
|
+
iut = SHExecutor::Executor.new({:application_path => "/bin/ls", :replace => false})
|
119
|
+
Kernel::last_command = "not executed"
|
120
|
+
iut.execute
|
121
|
+
expect(Kernel::last_command).to eq("not executed")
|
122
|
+
|
123
|
+
iut = SHExecutor::Executor.new({:application_path => "/bin/ls", :replace => 'blah ignore this'})
|
124
|
+
iut.execute
|
125
|
+
expect(Kernel::last_command).to eq("not executed")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when not waiting for completion' do
|
130
|
+
it 'should execute the command without blocking' do
|
131
|
+
test_command = "/bin/sleep"
|
132
|
+
test_params = ["2"]
|
133
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => test_command, :params => test_params})
|
134
|
+
before = Time.now
|
135
|
+
iut.execute
|
136
|
+
after = Time.now
|
137
|
+
expect(after - before).to be < 0.2
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should validate' do
|
141
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false})
|
142
|
+
expect{
|
143
|
+
iut.execute
|
144
|
+
}.to raise_error(ArgumentError, "No application path provided")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'when asking for the status of the executor' do
|
149
|
+
it 'should return "not executed" if execute has not been called' do
|
150
|
+
test_command = "/bin/sleep"
|
151
|
+
test_params = ["2"]
|
152
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => test_command, :params => test_params})
|
153
|
+
expect(iut.status).to eq("not executed")
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should return the current process status if the process is executing' do
|
157
|
+
test_command = "/bin/sleep"
|
158
|
+
test_params = ["2"]
|
159
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => test_command, :params => test_params})
|
160
|
+
iut.execute
|
161
|
+
status = iut.status
|
162
|
+
puts "status: #{status}"
|
163
|
+
running = ((status == "run") or (status == "sleep"))
|
164
|
+
expect(running).to eq(true)
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'should return "no longer executing" if the process has stopped for what-ever reason' do
|
168
|
+
test_command = "/bin/echo"
|
169
|
+
test_params = ["I ran"]
|
170
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => test_command, :params => test_params})
|
171
|
+
iut.execute
|
172
|
+
sleep 1
|
173
|
+
expect(iut.status).to eq("no longer executing")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'when asking for the result of execution' do
|
178
|
+
it 'should return nil if the process has not executed yet' do
|
179
|
+
test_command = "/bin/sleep"
|
180
|
+
test_params = ["1"]
|
181
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => test_command, :params => test_params})
|
182
|
+
expect(iut.result).to be_nil
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'should return nil if the process is still executing' do
|
186
|
+
test_command = "/bin/sleep"
|
187
|
+
test_params = ["1"]
|
188
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => test_command, :params => test_params})
|
189
|
+
iut.execute
|
190
|
+
expect(iut.result).to be_nil
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'should return a Process::Status object if the process has executed, but is no longer, for what-ever reason' do
|
194
|
+
test_command = "/bin/sleep"
|
195
|
+
test_params = ["1"]
|
196
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => false, :application_path => test_command, :params => test_params})
|
197
|
+
iut.execute
|
198
|
+
sleep 2
|
199
|
+
expect(iut.result.class).to eq(Process::Status)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
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
|
205
|
+
test_command = "/bin/sleep"
|
206
|
+
test_params = ["2"]
|
207
|
+
iut = SHExecutor::Executor.new({:timeout => 1, :wait_for_completion => true, :application_path => test_command, :params => test_params})
|
208
|
+
before = Time.now
|
209
|
+
expect {
|
210
|
+
iut.execute
|
211
|
+
}.to raise_error(Timeout::Error, "execution expired")
|
212
|
+
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")
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'should use Open3::popen3 with the command and parameters specified' do
|
227
|
+
test_command = "/bin/ls"
|
228
|
+
test_params = ["/tmp/"]
|
229
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command, :params => test_params})
|
230
|
+
stdin = stdout = stderr = StringIO.new
|
231
|
+
expect(Open3).to receive(:popen3).with(test_command, *test_params).and_return([stdin, stdout, stderr, Result.new(true)])
|
232
|
+
iut.execute
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'should use Open3::popen3 with the command' do
|
236
|
+
test_command = "/bin/ls"
|
237
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command})
|
238
|
+
stdin = stdout = stderr = StringIO.new
|
239
|
+
expect(Open3).to receive(:popen3).with(test_command).and_return([stdin, stdout, stderr, Result.new(true)])
|
240
|
+
iut.execute
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'should block until completion' do
|
244
|
+
test_command = "/bin/sleep"
|
245
|
+
test_params = ["2"]
|
246
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command, :params => test_params})
|
247
|
+
before = Time.now
|
248
|
+
iut.execute
|
249
|
+
after = Time.now
|
250
|
+
expect(after - before).to be > 2
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'should validate' do
|
254
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true})
|
255
|
+
expect{
|
256
|
+
iut.execute
|
257
|
+
}.to raise_error(ArgumentError, "No application path provided")
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
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
|
264
|
+
|
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
|
268
|
+
|
269
|
+
context 'when asked to redirect stdout' do
|
270
|
+
it 'should raise an exception if one of the file operations fails'
|
271
|
+
end
|
272
|
+
|
273
|
+
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
|
276
|
+
|
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
|
280
|
+
|
281
|
+
context 'when asked to redirect stderr' do
|
282
|
+
it 'should raise an exception if one of the file operations fails'
|
283
|
+
end
|
284
|
+
|
285
|
+
context 'when asked to flush' do
|
286
|
+
it 'should flush to its buffer for stderr' do
|
287
|
+
test_command = "/bin/echo"
|
288
|
+
test_params = ["hello world"]
|
289
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command, :params => test_params})
|
290
|
+
iut.execute
|
291
|
+
iut.flush
|
292
|
+
expect(iut.stdout).to eq("hello world\n")
|
293
|
+
expect(iut.stderr).to be_nil
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'should flush to its buffer for stdout' do
|
297
|
+
test_command = "/bin/ls"
|
298
|
+
test_params = ["/tmp/thisfiledoesnotexistsatgup80wh0hgoefhgohuo4whg4whg4w5hg0"]
|
299
|
+
iut = SHExecutor::Executor.new({:wait_for_completion => true, :application_path => test_command, :params => test_params})
|
300
|
+
iut.execute
|
301
|
+
iut.flush
|
302
|
+
expect(iut.stdout).to be_nil
|
303
|
+
expect(iut.stderr).to eq("ls: /tmp/thisfiledoesnotexistsatgup80wh0hgoefhgohuo4whg4whg4w5hg0: No such file or directory\n")
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Kernel
|
2
|
+
@@last_command = nil
|
3
|
+
@@last_params = nil
|
4
|
+
|
5
|
+
def exec(cmd, params = nil)
|
6
|
+
@@last_command = cmd
|
7
|
+
@@last_params = params
|
8
|
+
puts "Kernel::exec('#{cmd}', '#{params}')"
|
9
|
+
end
|
10
|
+
|
11
|
+
def last_command=(command)
|
12
|
+
@@last_command = command
|
13
|
+
end
|
14
|
+
|
15
|
+
def last_command
|
16
|
+
@@last_command
|
17
|
+
end
|
18
|
+
|
19
|
+
def last_params
|
20
|
+
@@last_params
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'rspec/mocks'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'simplecov'
|
5
|
+
require 'simplecov-rcov'
|
6
|
+
require 'byebug'
|
7
|
+
|
8
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
9
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
10
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
11
|
+
# loaded once.
|
12
|
+
#
|
13
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
14
|
+
|
15
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'executor'))
|
16
|
+
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.run_all_when_everything_filtered = true
|
19
|
+
config.filter_run :focus
|
20
|
+
|
21
|
+
# Run specs in random order to surface order dependencies. If you find an
|
22
|
+
# order dependency and want to debug it, you can fix the order by providing
|
23
|
+
# the seed, which is printed after each run.
|
24
|
+
# --seed 1234
|
25
|
+
config.order = 'random'
|
26
|
+
end
|
27
|
+
|
28
|
+
SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
|
29
|
+
SimpleCov.start do
|
30
|
+
add_filter "/spec/"
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shexecutor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ernst van Graan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov-rcov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Implements process replacement, forking, protection from shell injection,
|
98
|
+
and a variety of output options
|
99
|
+
email:
|
100
|
+
- ernst.van.graan@hetzner.co.za
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- .gitignore
|
106
|
+
- Gemfile
|
107
|
+
- Gemfile.lock
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- lib/shexecutor.rb
|
112
|
+
- lib/shexecutor/version.rb
|
113
|
+
- shexecutor.gemspec
|
114
|
+
- spec/executor_spec.rb
|
115
|
+
- spec/mocks/kernel.rb
|
116
|
+
- spec/mocks/result.rb
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
homepage: ''
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.0.14
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Execute shell commands easily and securely
|
142
|
+
test_files:
|
143
|
+
- spec/executor_spec.rb
|
144
|
+
- spec/mocks/kernel.rb
|
145
|
+
- spec/mocks/result.rb
|
146
|
+
- spec/spec_helper.rb
|