executor 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +42 -0
- data/Rakefile +1 -0
- data/executor.gemspec +24 -0
- data/lib/executor.rb +87 -0
- data/lib/executor/version.rb +3 -0
- data/spec/executor_spec.rb +146 -0
- data/spec/spec_helper.rb +3 -0
- metadata +89 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Executor
|
2
|
+
|
3
|
+
Executor aims to provide a standard interface to work with binaries outside of ruby. It provides a standard implementation of the following requirements:
|
4
|
+
|
5
|
+
* Capturing exit status and raising appropriate exception
|
6
|
+
* Redirecting stderr
|
7
|
+
* Asynchronous callbacks
|
8
|
+
* Logging of command output
|
9
|
+
|
10
|
+
## Options
|
11
|
+
|
12
|
+
* redirect_stderr - This will cause any stderr from a command to be merged to stdout
|
13
|
+
* raise_exceptions - This will listen for the exit code of the executed process and raise an exception if non 0
|
14
|
+
* async - This is true by default if a block is given to Executor::command, async will create a new thread and execute the passed block upon completion of the process
|
15
|
+
|
16
|
+
* logger - This takes an instance of logger (or an instance that implements #info)
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
### Basic
|
21
|
+
|
22
|
+
require 'executor'
|
23
|
+
|
24
|
+
Executor::command("echo 5")
|
25
|
+
|
26
|
+
### With redirection
|
27
|
+
|
28
|
+
Executor::command("expr 5 / 0", :redirect_stderr => true)
|
29
|
+
|
30
|
+
### With one-time configuration
|
31
|
+
|
32
|
+
Executor::configure(
|
33
|
+
:raise_exceptions => false
|
34
|
+
)
|
35
|
+
|
36
|
+
Executor::command("expr 5 / 0")
|
37
|
+
|
38
|
+
## Special notes
|
39
|
+
|
40
|
+
* In async mode (when block given or via explicit configuration), an exception if caught will be returned to the callback
|
41
|
+
|
42
|
+
credits: me, robgleeson@freenode
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/executor.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "executor/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "executor"
|
7
|
+
s.version = Executor::VERSION
|
8
|
+
s.authors = ["Thomas W. devol"]
|
9
|
+
s.email = ["vajrapani666@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Execute system commands through a configurable interface}
|
12
|
+
s.description = %q{Execute system commands with exception handling and logging}
|
13
|
+
|
14
|
+
s.rubyforge_project = "executor"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency "nullobject"
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "em-spec"
|
24
|
+
end
|
data/lib/executor.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require "executor/version"
|
2
|
+
require "nullobject"
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module Executor
|
6
|
+
|
7
|
+
ExecutorFailure = Class.new(StandardError)
|
8
|
+
CommandFailure = Class.new(ExecutorFailure)
|
9
|
+
TimeoutFailure = Class.new(ExecutorFailure)
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def method_missing(meth, *args, &block)
|
13
|
+
if Executor.respond_to?(meth, true)
|
14
|
+
Executor.send(meth, *args, &block)
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Executor
|
22
|
+
DEFAULTS = {
|
23
|
+
:redirect_stderr => false,
|
24
|
+
:raise_exceptions => true,
|
25
|
+
:async => false,
|
26
|
+
:logger => Null::Object.instance
|
27
|
+
}
|
28
|
+
class << self
|
29
|
+
attr_reader :config
|
30
|
+
|
31
|
+
def configure(config={})
|
32
|
+
@config = (@config || Executor::DEFAULTS).merge(config)
|
33
|
+
end
|
34
|
+
|
35
|
+
def command(cmd, options={}, &block)
|
36
|
+
options = configure.dup.merge(options)
|
37
|
+
options[:logger].info "COMMAND: #{cmd}"
|
38
|
+
options[:async] ||= block_given?
|
39
|
+
raise ArgumentError.new("No block given for async command") if (options[:async] && !block_given?)
|
40
|
+
|
41
|
+
if options[:async]
|
42
|
+
handle_async(cmd, options, &block)
|
43
|
+
else
|
44
|
+
return handle_nonasync(cmd, options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def handle_nonasync(cmd, options)
|
51
|
+
stmt = ["(#{cmd})"]
|
52
|
+
stmt << "2>&1" if @config[:redirect_stderr]
|
53
|
+
if options[:timeout]
|
54
|
+
Timeout::timeout(options[:timeout]) do
|
55
|
+
output = %x{#{stmt*" "}}
|
56
|
+
end
|
57
|
+
else
|
58
|
+
output = %x{#{stmt*" "}}
|
59
|
+
end
|
60
|
+
options[:logger].info "Non-async output for #{cmd}: #{output.inspect}"
|
61
|
+
return return_or_raise(cmd, $?.success?, output, options)
|
62
|
+
rescue Timeout::Error=>e
|
63
|
+
raise TimeoutFailure.new(e)
|
64
|
+
end
|
65
|
+
|
66
|
+
def handle_async(cmd, options, &block)
|
67
|
+
Thread.new do
|
68
|
+
IO.popen(cmd) do |pipe|
|
69
|
+
output = pipe.read
|
70
|
+
options[:logger].info "Async output for #{cmd}: #{output.inspect}"
|
71
|
+
pipe.close
|
72
|
+
yield return_or_raise(cmd, $?.success?, output, options) rescue $!
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def return_or_raise(cmd, success, output, options)
|
78
|
+
options[:logger].info "RETURN(#{cmd}): #{output}"
|
79
|
+
exception = CommandFailure.new(%Q{Command "#{cmd}" failed with #{output}})
|
80
|
+
return output if success
|
81
|
+
return exception if options[:async]
|
82
|
+
raise exception if options[:raise_exceptions]
|
83
|
+
return output
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'executor'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
describe Executor do
|
6
|
+
let (:failing_cmd) { "expr 5 / 0" }
|
7
|
+
context "exceptions" do
|
8
|
+
it "initializes" do
|
9
|
+
expect {
|
10
|
+
raise Executor::CommandFailure.new("foo")
|
11
|
+
}.to raise_exception(Executor::CommandFailure, "foo")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
context ".return_or_raise" do
|
15
|
+
|
16
|
+
let(:cmd) { "echo test"}
|
17
|
+
let(:output) { "test output" }
|
18
|
+
|
19
|
+
let(:options) { {:raise_exceptions => true, :logger=>stub(:info=>nil)} }
|
20
|
+
it "returns output if successful" do
|
21
|
+
output = "test"
|
22
|
+
Executor.return_or_raise("echo test",true, output, options).should == output
|
23
|
+
end
|
24
|
+
|
25
|
+
it "raises exceptions when configured" do
|
26
|
+
expect {
|
27
|
+
Executor.return_or_raise(cmd, false, output, options)
|
28
|
+
}.to raise_exception(Executor::CommandFailure, /#{cmd}.*#{output}/)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns an exception if async" do
|
32
|
+
Executor.return_or_raise(cmd, false, output, options.merge(:async=>true)).should be_an(Executor::CommandFailure)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
context ".command" do
|
36
|
+
context "instance configuration" do
|
37
|
+
it "overrides class-level config for a single command" do
|
38
|
+
Executor.configure(:raise_exceptions => false)
|
39
|
+
expect {
|
40
|
+
Executor.command(failing_cmd, :raise_exceptions=>true)
|
41
|
+
}.to raise_exception(Executor::CommandFailure)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
context "async" do
|
45
|
+
before do
|
46
|
+
Executor.configure(
|
47
|
+
:raise_exceptions => true
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "uses a separate thread" do
|
52
|
+
i = 0
|
53
|
+
Executor.command("sleep 3 && echo 5") do |result|
|
54
|
+
i+=1
|
55
|
+
end
|
56
|
+
i.should == 0
|
57
|
+
sleep 5
|
58
|
+
i.should == 1
|
59
|
+
end
|
60
|
+
|
61
|
+
it "calls the block passed to it" do
|
62
|
+
block_called = Executor.command(failing_cmd) do |result|
|
63
|
+
true
|
64
|
+
end || false
|
65
|
+
block_called.should be_true
|
66
|
+
end
|
67
|
+
it "should raise an exception upon non zero exit" do
|
68
|
+
Executor.command(failing_cmd) do |result|
|
69
|
+
result.should be_kind_of Executor::CommandFailure
|
70
|
+
end
|
71
|
+
end
|
72
|
+
it "with explicit async config, raise exception if no block given" do
|
73
|
+
expect {
|
74
|
+
Executor.command(failing_cmd, :async=>true)
|
75
|
+
}.to raise_exception(ArgumentError)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
context "redirection" do
|
79
|
+
before do
|
80
|
+
Executor.configure(
|
81
|
+
:redirect_stderr => true,
|
82
|
+
:raise_exceptions => false
|
83
|
+
)
|
84
|
+
end
|
85
|
+
it "redirects stderr" do
|
86
|
+
result = Executor.command(failing_cmd)
|
87
|
+
result.should =~ /expr: division by zero/
|
88
|
+
end
|
89
|
+
end
|
90
|
+
context "non-async" do
|
91
|
+
before do
|
92
|
+
Executor.configure(
|
93
|
+
:raise_exceptions => true
|
94
|
+
)
|
95
|
+
end
|
96
|
+
it "should raise an exception upon non zero exit" do
|
97
|
+
expect {
|
98
|
+
Executor.command(failing_cmd)
|
99
|
+
}.to raise_exception(Executor::CommandFailure)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
context "logging" do
|
104
|
+
it "should log errors" do
|
105
|
+
logger = Logger.new(StringIO.new)
|
106
|
+
logger.should_receive(:info).with(instance_of(String)).any_number_of_times
|
107
|
+
Executor.configure(
|
108
|
+
:logger => logger,
|
109
|
+
:raise_exceptions => false
|
110
|
+
)
|
111
|
+
Executor.command("expr 5 / 0")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
context "without options" do
|
115
|
+
it "should be able to execute 'echo x' " do
|
116
|
+
Executor.command(%Q{echo "x"})
|
117
|
+
end
|
118
|
+
it "should be able to execute sleep && echo " do
|
119
|
+
start = Time.now.to_i
|
120
|
+
Executor.command("sleep 2 && echo 'test'") do |result|
|
121
|
+
finish = Time.now.to_i
|
122
|
+
(finish-start).should be_greater_than(2)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
describe "on the roadmap" do
|
129
|
+
pending "with eventmachine" do
|
130
|
+
it "uses em::popen"
|
131
|
+
end
|
132
|
+
pending "timeouts" do
|
133
|
+
it "should capture timeouts" do
|
134
|
+
expect {
|
135
|
+
Executor.command("sleep 5", :timeout=>2)
|
136
|
+
}.to raise_exception(Executor::TimeoutFailure)
|
137
|
+
end
|
138
|
+
it "should fail for timeout on async" do
|
139
|
+
Executor.command("sleep 5", :timeout => 2) do |result|
|
140
|
+
raise result
|
141
|
+
end
|
142
|
+
sleep 5
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: executor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Thomas W. devol
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-28 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: nullobject
|
16
|
+
requirement: &70114217923400 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70114217923400
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &70114217922980 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70114217922980
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: em-spec
|
38
|
+
requirement: &70114217922560 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70114217922560
|
47
|
+
description: Execute system commands with exception handling and logging
|
48
|
+
email:
|
49
|
+
- vajrapani666@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- executor.gemspec
|
59
|
+
- lib/executor.rb
|
60
|
+
- lib/executor/version.rb
|
61
|
+
- spec/executor_spec.rb
|
62
|
+
- spec/spec_helper.rb
|
63
|
+
homepage: ''
|
64
|
+
licenses: []
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project: executor
|
83
|
+
rubygems_version: 1.8.10
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Execute system commands through a configurable interface
|
87
|
+
test_files:
|
88
|
+
- spec/executor_spec.rb
|
89
|
+
- spec/spec_helper.rb
|