subprocess 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +46 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +77 -0
- data/Rakefile +20 -0
- data/TODO.rdoc +1 -0
- data/examples/simple.irb +22 -0
- data/examples/simple_timeout.irb +22 -0
- data/features/multiple_popens_sequence.feature +23 -0
- data/features/popen.feature +45 -0
- data/features/popen_over_ssh.feature +44 -0
- data/features/popen_over_ssh_without_blocking.feature +16 -0
- data/features/popen_remote_fails_with_invalid_auth_data.feature +13 -0
- data/features/popen_reports_runtime.feature +11 -0
- data/features/popen_running.feature +11 -0
- data/features/popen_with_timeout.feature +19 -0
- data/features/popen_without_blocking.feature +16 -0
- data/features/step_definitions/common_steps.rb +168 -0
- data/features/step_definitions/multiple_popens_sequence_steps.rb +73 -0
- data/features/step_definitions/popen_over_ssh_steps.rb +29 -0
- data/features/step_definitions/popen_over_ssh_without_blocking_steps.rb +30 -0
- data/features/step_definitions/popen_remote_fails_with_invalid_auth_dat_steps.rb +19 -0
- data/features/step_definitions/popen_reports_runtime_steps.rb +13 -0
- data/features/step_definitions/popen_running_steps.rb +12 -0
- data/features/step_definitions/popen_steps.rb +34 -0
- data/features/step_definitions/popen_with_timeout_steps.rb +24 -0
- data/features/step_definitions/popen_without_blocking_steps.rb +33 -0
- data/features/support/common.rb +29 -0
- data/features/support/env.rb +15 -0
- data/features/support/matchers.rb +11 -0
- data/lib/core_ext/hash.rb +14 -0
- data/lib/core_ext/process_status.rb +14 -0
- data/lib/subprocess/popen.rb +188 -0
- data/lib/subprocess/popen_factory.rb +63 -0
- data/lib/subprocess/popen_remote.rb +64 -0
- data/lib/subprocess/popen_sequence.rb +57 -0
- data/lib/subprocess.rb +23 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/subprocess/popen_spec.rb +32 -0
- data/spec/subprocess_spec.rb +2 -0
- data/subprocess.gemspec +36 -0
- data/tasks/rspec.rake +21 -0
- metadata +138 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
Given /^I have a new remote Subprocess instance initialized with "([^\"]*)"$/ do |command|
|
3
|
+
@popen_remote = Subprocess::PopenRemote.new(command, 'localhost', 'popen', 10, :password => 'popen')
|
4
|
+
end
|
5
|
+
|
6
|
+
When /^I invoke the run method of said remote subprocess$/ do
|
7
|
+
@popen_remote.run
|
8
|
+
end
|
9
|
+
|
10
|
+
When /^I invoke the wait method of said remote subprocess$/ do
|
11
|
+
@popen_remote.wait
|
12
|
+
end
|
13
|
+
|
14
|
+
Then /^the remote instances exit status is "([^\"]*)"$/ do |exitstatus|
|
15
|
+
@popen_remote.status[:exitstatus].should == exitstatus.to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
Then /^the remote instances stdout matches "([^\"]*)"$/ do |stdout|
|
19
|
+
@popen_remote.stdout.should match(stdout)
|
20
|
+
end
|
21
|
+
|
22
|
+
Then /^the remote instances stderr matches "([^\"]*)"$/ do |stderr|
|
23
|
+
@popen_remote.stderr.should match(stderr)
|
24
|
+
end
|
25
|
+
|
26
|
+
Then /^the remote instance should have a numerical pid$/ do
|
27
|
+
@popen_remote.pid.should be_a_kind_of Fixnum
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
Given /^I have a new remote nonblocking subprocess that takes a long time to run$/ do
|
3
|
+
@popen = Subprocess::PopenRemote.new('sleep 3 && exit 1', 'localhost', nil, 10, 'popen', :password => 'popen')
|
4
|
+
@popen.should_not be_nil
|
5
|
+
end
|
6
|
+
|
7
|
+
When /^I invoke the run method of said nonblocking remote subprocess$/ do
|
8
|
+
start_time = Time.now.to_i
|
9
|
+
@popen.run
|
10
|
+
@total_time = Time.now.to_i - start_time
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^the remote nonblocking subprocess should not block$/ do
|
14
|
+
@total_time.should be_close(0, 2)
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^the remote nonblocking subprocess should report its run status$/ do
|
18
|
+
@popen.should respond_to(:running?)
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /^the remote nonblocking subprocess should support being waited on till complete$/ do
|
22
|
+
@popen.wait
|
23
|
+
end
|
24
|
+
|
25
|
+
Then /^the remote nonblocking subprocess should have status info$/ do
|
26
|
+
@popen.status[:exitstatus].should be_kind_of Numeric
|
27
|
+
@popen.status.should be_a_kind_of Hash
|
28
|
+
end
|
29
|
+
|
30
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Given /^I have a new remote subproces with invalid username$/ do
|
2
|
+
@popen_remote = Subprocess::PopenRemote.new('tail -n50 /var/log/daemon.log',
|
3
|
+
'localhost', 'someadminnamethatshouldneverexist', 300,
|
4
|
+
:password => 'somebadpasswordnooneuses')
|
5
|
+
end
|
6
|
+
|
7
|
+
Given /^invalid password$/ do
|
8
|
+
# by leaving this empty its an auto pass and really just serves to make feature read better
|
9
|
+
end
|
10
|
+
|
11
|
+
When /^I run the remote subprocess$/ do
|
12
|
+
@popen_remote.run
|
13
|
+
end
|
14
|
+
|
15
|
+
Then /^the remote subprocess should return an error$/ do
|
16
|
+
@popen_remote.wait
|
17
|
+
#@popen_remote.stderr.should_not be_nil
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Given /^I have a new subprocess that takes 3 seconds$/ do
|
2
|
+
@popen = Subprocess::Popen.new('sleep 3')
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I wait on said 3 second process to complete$/ do
|
6
|
+
@popen.run
|
7
|
+
@popen.wait
|
8
|
+
end
|
9
|
+
|
10
|
+
Then /^the subprocess should report a run time of around 3 seconds$/ do
|
11
|
+
@popen.run_time.should be_close(3, 0.2)
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Given /^I have a new subprocess that runs fast$/ do
|
2
|
+
@popen = Subprocess::Popen.new('echo 1')
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I invoke the run method of said fast subprocess$/ do
|
6
|
+
@popen.run
|
7
|
+
end
|
8
|
+
|
9
|
+
Then /^the subprocess should report running as false without waiting$/ do
|
10
|
+
@popen.running?.should be_false
|
11
|
+
end
|
12
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
Given /^I have a new Subprocess instance initialized with "([^\"]*)"$/ do |command|
|
2
|
+
@popen = Subprocess::Popen.new(command)
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I invoke the run method of said subprocess$/ do
|
6
|
+
@popen.run
|
7
|
+
end
|
8
|
+
|
9
|
+
When /^I invoke the wait method of said subprocess$/ do
|
10
|
+
@popen.wait
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^the instance should have a status attribute$/ do
|
14
|
+
@popen.status.should be_a_kind_of Hash
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^the instances exit status is "([^\"]*)"$/ do |exitstatus|
|
18
|
+
@popen.status[:exitstatus].should == exitstatus.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /^the instances stdout matches "([^\"]*)"$/ do |stdout|
|
22
|
+
@popen.stdout.should match(stdout)
|
23
|
+
end
|
24
|
+
|
25
|
+
Then /^the instances stderr matches "([^\"]*)"$/ do |stderr|
|
26
|
+
@popen.stderr.should match(stderr)
|
27
|
+
end
|
28
|
+
|
29
|
+
Then /^the instance should have a numerical pid$/ do
|
30
|
+
@popen.pid.should be_a_kind_of Fixnum
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Given /^I have a new subprocess that takes more than 5 seconds to run$/ do
|
2
|
+
@popen = Subprocess::Popen.new('sleep 10', 5)
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^I have a new subprocess that takes less than 5 seconds to run$/ do
|
6
|
+
@popen = Subprocess::Popen.new('sleep 1', 5)
|
7
|
+
end
|
8
|
+
|
9
|
+
When /^I invoke the run method of said subprocess with timeout$/ do
|
10
|
+
@popen.run
|
11
|
+
@popen.wait
|
12
|
+
end
|
13
|
+
|
14
|
+
Given /^I set a timeout of 5 seconds$/ do
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^the subprocess should exit with exitcode 1$/ do
|
18
|
+
@popen.status[:exitstatus].should == 1
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /^the subprocess should complete fine$/ do
|
22
|
+
@popen.running?.should be_false
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
Given /^I have a new subprocess that takes a long time to run$/ do
|
3
|
+
@popen = Subprocess::Popen.new('sleep 3 && exit 1')
|
4
|
+
@popen.should_not be_nil
|
5
|
+
end
|
6
|
+
|
7
|
+
When /^I invoke the run method of said nonblocking subprocess$/ do
|
8
|
+
start_time = Time.now.to_i
|
9
|
+
@popen.run
|
10
|
+
@total_time = Time.now.to_i - start_time
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^the subprocess should not block$/ do
|
14
|
+
# this should pass if we backgrounded cause it shouldn't take more than
|
15
|
+
# 1 second to get here but the command should take 3 seconds to run
|
16
|
+
@total_time.should be_close(0, 2)
|
17
|
+
end
|
18
|
+
|
19
|
+
Then /^the subprocess should report its run status$/ do
|
20
|
+
@popen.should respond_to(:running?)
|
21
|
+
end
|
22
|
+
|
23
|
+
Then /^the subprocess should support being waited on till complete$/ do
|
24
|
+
@popen.wait
|
25
|
+
@popen.status[:exitstatus].should be_kind_of Numeric
|
26
|
+
end
|
27
|
+
|
28
|
+
Then /^the subprocess should have status info$/ do
|
29
|
+
@popen.status.should be_a_kind_of Hash
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CommonHelpers
|
2
|
+
def in_tmp_folder(&block)
|
3
|
+
FileUtils.chdir(@tmp_root, &block)
|
4
|
+
end
|
5
|
+
|
6
|
+
def in_project_folder(&block)
|
7
|
+
project_folder = @active_project_folder || @tmp_root
|
8
|
+
FileUtils.chdir(project_folder, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def in_home_folder(&block)
|
12
|
+
FileUtils.chdir(@home_path, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def force_local_lib_override(project_name = @project_name)
|
16
|
+
rakefile = File.read(File.join(project_name, 'Rakefile'))
|
17
|
+
File.open(File.join(project_name, 'Rakefile'), "w+") do |f|
|
18
|
+
f << "$:.unshift('#{@lib_path}')\n"
|
19
|
+
f << rakefile
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup_active_project_folder project_name
|
24
|
+
@active_project_folder = File.join(@tmp_root, project_name)
|
25
|
+
@project_name = project_name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
World(CommonHelpers)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../../lib/subprocess"
|
2
|
+
|
3
|
+
gem 'cucumber'
|
4
|
+
require 'cucumber'
|
5
|
+
gem 'rspec', '<= 1.3.0'
|
6
|
+
require 'spec'
|
7
|
+
|
8
|
+
Before do
|
9
|
+
@tmp_root = File.dirname(__FILE__) + "/../../tmp"
|
10
|
+
@home_path = File.expand_path(File.join(@tmp_root, "home"))
|
11
|
+
@lib_path = File.expand_path(File.dirname(__FILE__) + "/../../lib")
|
12
|
+
FileUtils.rm_rf @tmp_root
|
13
|
+
FileUtils.mkdir_p @home_path
|
14
|
+
ENV['HOME'] = @home_path
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Matchers
|
2
|
+
def contain(expected)
|
3
|
+
simple_matcher("contain #{expected.inspect}") do |given, matcher|
|
4
|
+
matcher.failure_message = "expected #{given.inspect} to contain #{expected.inspect}"
|
5
|
+
matcher.negative_failure_message = "expected #{given.inspect} not to contain #{expected.inspect}"
|
6
|
+
given.index expected
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
World(Matchers)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
class Hash
|
3
|
+
# str8 from activesupport core_ext/hash/keys.rb
|
4
|
+
def symbolize_keys
|
5
|
+
inject({}) do |options, (key, value)|
|
6
|
+
options[(key.to_sym rescue key) || key] = value
|
7
|
+
options
|
8
|
+
end
|
9
|
+
end
|
10
|
+
def symbolize_keys!
|
11
|
+
self.replace(self.symbolize_keys)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
module Process
|
3
|
+
class Status
|
4
|
+
def to_hash
|
5
|
+
{ :exited? => exited?, :exitstatus => exitstatus, :pid => pid,
|
6
|
+
:stopped? => stopped?, :stopsig => stopsig, :success? => success?,
|
7
|
+
:termsig => termsig, :timed_out? => false }
|
8
|
+
end
|
9
|
+
def to_json
|
10
|
+
to_hash.to_json
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Subprocess
|
2
|
+
class Popen
|
3
|
+
include Timeout
|
4
|
+
|
5
|
+
attr_accessor :command, :stdout, :stderr, :status, :timeout
|
6
|
+
|
7
|
+
def initialize(command, timeout=300)
|
8
|
+
self.command = command
|
9
|
+
self.timeout = timeout
|
10
|
+
@running = false
|
11
|
+
@ipc_parsed = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def running?
|
15
|
+
# return false if no parent pid or if running is already false
|
16
|
+
return false unless @parent_pid
|
17
|
+
return @running unless @running
|
18
|
+
|
19
|
+
begin
|
20
|
+
# see if the process is running or not
|
21
|
+
Process.kill(0, @parent_pid)
|
22
|
+
# since we didn't error then we have a pid running
|
23
|
+
# so lets see if is over after less than .5 seconds
|
24
|
+
begin
|
25
|
+
Timeout::timeout(0.5) do
|
26
|
+
@parent_pid, parent_status = Process.wait2(@parent_pid, 0)
|
27
|
+
end
|
28
|
+
rescue Timeout::Error
|
29
|
+
# wait timed out so so pid is stll running
|
30
|
+
@running = true
|
31
|
+
end
|
32
|
+
# no timeout so pid is finished
|
33
|
+
@running = false
|
34
|
+
rescue Errno::ESRCH
|
35
|
+
# Process.kill says the pid is not found
|
36
|
+
@running = false
|
37
|
+
end
|
38
|
+
|
39
|
+
# parse the child status if pid is complete
|
40
|
+
parse_ipc_pipe unless (@running or @ipc_parsed)
|
41
|
+
@running
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_time
|
45
|
+
defined?(:@status) ? @status[:run_time] : false
|
46
|
+
end
|
47
|
+
|
48
|
+
def pid
|
49
|
+
defined?(:@status) ? @status[:pid] : false
|
50
|
+
end
|
51
|
+
|
52
|
+
def perform
|
53
|
+
# delayed job anyone?
|
54
|
+
run unless running?
|
55
|
+
wait
|
56
|
+
end
|
57
|
+
|
58
|
+
def run
|
59
|
+
setup_pipes
|
60
|
+
# record the time, set running and fork off the command
|
61
|
+
@start_time = Time.now.to_f
|
62
|
+
@running = true
|
63
|
+
@parent_pid = fork_parent
|
64
|
+
|
65
|
+
# close up the pipes
|
66
|
+
[ @stdin_rd, @stdout_wr, @stderr_wr, @ipc_wr ].each do |p|
|
67
|
+
p.close
|
68
|
+
end
|
69
|
+
|
70
|
+
# make sure sure the stdin_wr handle is sync
|
71
|
+
@stdin_wr.sync = true
|
72
|
+
end
|
73
|
+
|
74
|
+
def wait
|
75
|
+
# block until the process completes
|
76
|
+
@parent_pid, parent_status = Process.wait2(@parent_pid)
|
77
|
+
@running = false
|
78
|
+
parse_ipc_pipe unless @ipc_parsed
|
79
|
+
end
|
80
|
+
|
81
|
+
def active_record?
|
82
|
+
Module.constants.include?("ActiveRecord")
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
def setup_pipes
|
87
|
+
# setup stream pipes and a pipe for interprocess communication
|
88
|
+
@stdin_rd, @stdin_wr = IO::pipe
|
89
|
+
@stdout_rd, @stdout_wr = IO::pipe
|
90
|
+
@stderr_rd, @stderr_wr = IO::pipe
|
91
|
+
@ipc_rd, @ipc_wr = IO::pipe
|
92
|
+
end
|
93
|
+
|
94
|
+
def ar_remove_connection
|
95
|
+
ActiveRecord::Base.remove_connection
|
96
|
+
end
|
97
|
+
|
98
|
+
def ar_establish_connection(dbconfig)
|
99
|
+
ActiveRecord::Base.establish_connection(dbconfig)
|
100
|
+
end
|
101
|
+
|
102
|
+
def fork_parent
|
103
|
+
dbconfig = ar_remove_connection if active_record?
|
104
|
+
pid = Kernel.fork {
|
105
|
+
ar_establish_connection(dbconfig) if active_record?
|
106
|
+
begin
|
107
|
+
redirect_stdstreams
|
108
|
+
report_child_status_to_parent(fork_child)
|
109
|
+
teardown_pipes
|
110
|
+
ensure
|
111
|
+
ar_remove_connection if active_record?
|
112
|
+
end
|
113
|
+
}
|
114
|
+
ar_establish_connection(dbconfig) if active_record?
|
115
|
+
pid
|
116
|
+
end
|
117
|
+
|
118
|
+
def redirect_stdstreams
|
119
|
+
# close the pipes we don't need
|
120
|
+
[ @stdin_wr, @stdout_rd, @stderr_rd ].each do |p|
|
121
|
+
p.close
|
122
|
+
end
|
123
|
+
|
124
|
+
# redirect this forks std streams into pipes
|
125
|
+
STDIN.reopen(@stdin_rd)
|
126
|
+
STDOUT.reopen(@stdout_wr)
|
127
|
+
STDERR.reopen(@stderr_wr)
|
128
|
+
end
|
129
|
+
|
130
|
+
def fork_child
|
131
|
+
child_status = Hash.new
|
132
|
+
child_pid = nil
|
133
|
+
start_time = Time.now.to_f
|
134
|
+
begin
|
135
|
+
Timeout::timeout(self.timeout) do
|
136
|
+
dbconfig = ar_remove_connection if active_record?
|
137
|
+
begin
|
138
|
+
child_pid = Kernel.fork{
|
139
|
+
fork_child_exec
|
140
|
+
}
|
141
|
+
ensure
|
142
|
+
ar_establish_connection(dbconfig) if active_record?
|
143
|
+
end
|
144
|
+
child_status = Process.wait2(child_pid)[1]
|
145
|
+
end
|
146
|
+
rescue Timeout::Error
|
147
|
+
begin
|
148
|
+
Process.kill('KILL', child_pid)
|
149
|
+
rescue Errno::ESRCH
|
150
|
+
end
|
151
|
+
child_status[:exitstatus] = 1
|
152
|
+
child_status[:timed_out?] = true
|
153
|
+
end
|
154
|
+
child_status = child_status.to_hash
|
155
|
+
child_status[:run_time] = Time.now.to_f - start_time
|
156
|
+
child_status
|
157
|
+
end
|
158
|
+
|
159
|
+
def report_child_status_to_parent(child_status)
|
160
|
+
@ipc_wr.write child_status.to_json
|
161
|
+
@ipc_wr.flush
|
162
|
+
end
|
163
|
+
|
164
|
+
def teardown_pipes
|
165
|
+
# close pipes
|
166
|
+
[ @stdin_rd, @stdout_wr, @stderr_wr, @ipc_wr ].each do |p|
|
167
|
+
p.close
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def fork_child_exec
|
172
|
+
Kernel.exec(@command)
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_ipc_pipe
|
176
|
+
begin
|
177
|
+
@status = JSON.parse(@ipc_rd.read).symbolize_keys
|
178
|
+
rescue StandardError => err
|
179
|
+
@status = { :exitstatus => 1, :timed_out? => false, :error => err }
|
180
|
+
end
|
181
|
+
@stdout, @stderr = @stdout_rd.read.chomp, @stderr_rd.read.chomp
|
182
|
+
@stdout_rd.close; @stderr_rd.close; @ipc_rd.close
|
183
|
+
@ipc_parsed = true
|
184
|
+
end
|
185
|
+
|
186
|
+
end # class Popen
|
187
|
+
end # module Subprocess
|
188
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Subprocess
|
4
|
+
module PopenFactoryMixin
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def def_command(name, command, class_meths=true)
|
13
|
+
name = class_meths ? "self.#{name}" : name
|
14
|
+
instance_eval "
|
15
|
+
def #{name}(*args)
|
16
|
+
Subprocess::Popen.new('#{command}', *args)
|
17
|
+
end
|
18
|
+
", __FILE__, __LINE__
|
19
|
+
end
|
20
|
+
|
21
|
+
def def_dynamic_command(name, command, class_meths=true)
|
22
|
+
name = class_meths ? "self.#{name}" : name
|
23
|
+
instance_eval "
|
24
|
+
def #{name}(data, *args)
|
25
|
+
command = lambda do |data|
|
26
|
+
\"#{command}\"
|
27
|
+
end
|
28
|
+
Subprocess::Popen.new(command.call(data), *args)
|
29
|
+
end
|
30
|
+
", __FILE__, __LINE__
|
31
|
+
end
|
32
|
+
end # ClassMethods
|
33
|
+
end # PopenFactoryMixin
|
34
|
+
|
35
|
+
module PopenRemoteFactoryMixin
|
36
|
+
|
37
|
+
def self.included(base)
|
38
|
+
base.extend(ClassMethods)
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
|
43
|
+
def def_command(name, command, hostname_var, username_var, ssh_params_var, timeout=300)
|
44
|
+
instance_eval do
|
45
|
+
define_method name.to_sym do
|
46
|
+
eval("Subprocess::PopenRemote.new('#{command}', #{hostname_var}, #{username_var}, timeout, *#{ssh_params_var})")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def def_dynamic_command(name, command, hostname_var, username_var, ssh_params_var, timeout=300)
|
52
|
+
instance_eval do
|
53
|
+
define_method name.to_sym do |data|
|
54
|
+
cmd = ERB.new(command).result(binding)
|
55
|
+
eval("Subprocess::PopenRemote.new(cmd, #{hostname_var}, #{username_var}, timeout, *#{ssh_params_var})")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end # ClassMethods
|
60
|
+
end # PopenRemoteFactoryMixin
|
61
|
+
|
62
|
+
end # Subprocess
|
63
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
module Subprocess
|
3
|
+
|
4
|
+
class PopenRemote < Popen
|
5
|
+
attr_accessor :hostname, :ssh_params
|
6
|
+
|
7
|
+
def initialize(command, hostname, username, timeout=300, *ssh_params)
|
8
|
+
@command = command
|
9
|
+
@timeout = timeout
|
10
|
+
@running = false
|
11
|
+
@ipc_parsed = false
|
12
|
+
|
13
|
+
@hostname = hostname
|
14
|
+
@username = username
|
15
|
+
@ssh_params = ssh_params
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def log_to_stderr_and_exit(msg)
|
20
|
+
$stderr.write("Net::SSH error: #{@hostname} #{msg}")
|
21
|
+
exit 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def fork_child_exec
|
25
|
+
exit_status = 0
|
26
|
+
begin
|
27
|
+
ssh = Net::SSH.start(@hostname, @username, *@ssh_params) do |ssh|
|
28
|
+
ssh.open_channel do |channel|
|
29
|
+
channel.exec(@command) do |chan, success|
|
30
|
+
log_to_stderr_and_exit('failed to open exec channel') unless success
|
31
|
+
|
32
|
+
channel.on_data do |chan, data|
|
33
|
+
$stdout.write data
|
34
|
+
end
|
35
|
+
|
36
|
+
channel.on_extended_data do |chan, type, data|
|
37
|
+
$stderr.write data
|
38
|
+
end
|
39
|
+
|
40
|
+
channel.on_request('exit-status') do |chan, data|
|
41
|
+
exit_status = data.read_long.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
ssh.loop
|
47
|
+
end
|
48
|
+
rescue Net::SSH::AuthenticationFailed
|
49
|
+
log_to_stderr_and_exit("authentication failure\n")
|
50
|
+
rescue Errno::ECONNREFUSED
|
51
|
+
log_to_stderr_and_exit("connection refused\n")
|
52
|
+
rescue Errno::ETIMEDOUT
|
53
|
+
log_to_stderr_and_exit("connection timeout\n")
|
54
|
+
rescue Errno::EHOSTUNREACH
|
55
|
+
log_to_stderr_and_exit("unreachable\n")
|
56
|
+
rescue StandardError => error
|
57
|
+
log_to_stderr_and_exit("error: #{error.message}\n")
|
58
|
+
end
|
59
|
+
exit exit_status
|
60
|
+
end
|
61
|
+
|
62
|
+
end # class PopenRemote
|
63
|
+
end # module Subprocess
|
64
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
module Subprocess
|
3
|
+
class PopenSequenceError < StandardError; end
|
4
|
+
class PopenSequence
|
5
|
+
require 'forwardable'
|
6
|
+
extend Forwardable
|
7
|
+
attr_reader :running, :queue, :complete, :incomplete, :failed
|
8
|
+
|
9
|
+
def_delegators :@last, :stdout, :stderr, :status
|
10
|
+
|
11
|
+
def initialize(queue=[])
|
12
|
+
@queue = queue
|
13
|
+
@incomplete = queue
|
14
|
+
@complete = []
|
15
|
+
@failed = []
|
16
|
+
@running = false
|
17
|
+
@completed = false
|
18
|
+
@last = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def include?(item)
|
22
|
+
@queue.include?(item)
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](index)
|
26
|
+
@queue[index]
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_popen(popen)
|
30
|
+
raise PopenSequenceError,
|
31
|
+
"appending a completed sequence is not allowed" if @completed
|
32
|
+
@incomplete << popen
|
33
|
+
end
|
34
|
+
alias :<< :add_popen
|
35
|
+
|
36
|
+
def perform
|
37
|
+
@running = true
|
38
|
+
@failure = false
|
39
|
+
@queue.each do |popen|
|
40
|
+
@last = popen
|
41
|
+
popen.perform
|
42
|
+
popen.status[:exitstatus] == 0 ? @complete << popen :
|
43
|
+
(@failed << popen; break)
|
44
|
+
end
|
45
|
+
@incomplete = @incomplete - @complete
|
46
|
+
@incomplete = @incomplete - @failed
|
47
|
+
@running = false
|
48
|
+
@completed = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def completed?
|
52
|
+
@completed
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/lib/subprocess.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'net/ssh'
|
6
|
+
require 'json'
|
7
|
+
rescue LoadError
|
8
|
+
require 'rubygems'
|
9
|
+
require 'net/ssh'
|
10
|
+
require 'json'
|
11
|
+
end
|
12
|
+
require 'timeout'
|
13
|
+
|
14
|
+
module Subprocess
|
15
|
+
VERSION = '0.1.6'
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'core_ext/hash'
|
19
|
+
require 'core_ext/process_status'
|
20
|
+
require 'subprocess/popen'
|
21
|
+
require 'subprocess/popen_remote'
|
22
|
+
require 'subprocess/popen_sequence'
|
23
|
+
require 'subprocess/popen_factory'
|