subprocess 0.1.6
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/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'
|