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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in executor.gemspec
4
+ gemspec
@@ -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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -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,3 @@
1
+ module Executor
2
+ VERSION = "0.0.1"
3
+ 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
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__),'..','lib')
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__))
3
+
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