Skip to content

Commit

Permalink
Add a PID handler.
Browse files Browse the repository at this point in the history
Prior to this the PID of the running process was
invisible to the caller of popen3.  An optional
handler notifies the caller as soon as the PID
is known.

Add pid_handler to example.
  • Loading branch information
Graham Hughes committed Aug 27, 2010
1 parent 425dac8 commit 34a1f7b
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 9 deletions.
7 changes: 7 additions & 0 deletions README.rdoc
Expand Up @@ -26,6 +26,11 @@ to report issues.
@stdout_text = ""
@stderr_text = ""
@exit_status = nil
@pid = nil

def on_pid(pid)
@pid = pid
end

def on_read_stdout(data)
@stdout_text << data
Expand All @@ -45,6 +50,7 @@ to report issues.
RightScale.popen3(:command => command,
:target => self,
:environment => nil,
:pid_handler => :on_pid,
:stdout_handler => :on_read_stdout,
:stderr_handler => :on_read_stderr,
:exit_handler => :on_exit)
Expand All @@ -60,6 +66,7 @@ to report issues.
puts "@stdout_text = #{@stdout_text}"
puts "@stderr_text = #{@stderr_text}"
puts "@exit_status.exitstatus = #{@exit_status.exitstatus}"
puts "@pid = #{@pid}"


== INSTALLATION
Expand Down
8 changes: 6 additions & 2 deletions lib/linux/right_popen.rb
Expand Up @@ -110,6 +110,7 @@ def force_detach
# standard streams of the child process.
#
# === Parameters
# options[:pid_handler](Symbol):: Token for pid handler method name.
# options[:temp_dir]:: Path to temporary directory where executable files are
# created, default to /tmp if not specified
#
Expand Down Expand Up @@ -140,7 +141,10 @@ def self.popen3_imp(options)
end

# Launch child process
EM.popen(exec_file, StdOutHandler, options, c, r, w)
connection = EM.popen(exec_file, StdOutHandler, options, c, r, w)
if options[:pid_handler]
options[:target].method(options[:pid_handler]).call(EM.get_subprocess_pid(connection.signature))
end

# Restore environment variables
unless envs.empty?
Expand All @@ -149,7 +153,7 @@ def self.popen3_imp(options)
end

# Do not close 'w', strange things happen otherwise
# (command protocol socket gets closed during decommission)
# (command protocol socket gets closed during decommission)
$stderr.reopen saved_stderr
end
true
Expand Down
3 changes: 2 additions & 1 deletion lib/right_popen.rb
Expand Up @@ -49,6 +49,7 @@ module RightScale
# options[:environment](Hash):: Hash of environment variables values keyed by name
# options[:input](String):: Input string that will get streamed into child's process stdin
# options[:target](Object):: object defining handler methods to be called, optional (no handlers can be defined if not specified)
# options[:pid_handler](String):: PID notification handler method name, optional
# options[:stdout_handler](String):: Stdout handler method name, optional
# options[:stderr_handler](String):: Stderr handler method name, optional
# options[:exit_handler](String):: Exit handler method name, optional
Expand All @@ -58,7 +59,7 @@ module RightScale
def self.popen3(options)
raise "EventMachine reactor must be started" unless EM.reactor_running?
raise "Missing command" unless options[:command]
raise "Missing target" unless options[:target] || !options[:stdout_handler] && !options[:stderr_handler] && !options[:exit_handler]
raise "Missing target" unless options[:target] || !options[:stdout_handler] && !options[:stderr_handler] && !options[:exit_handler] && !options[:pid_handler]
RightScale.popen3_imp(options)
true
end
Expand Down
5 changes: 4 additions & 1 deletion lib/win32/right_popen.rb
Expand Up @@ -217,7 +217,10 @@ def self.popen3_imp(options)
# streams aren't used directly by the connectors except that they are closed
# on unbind.
stderr_eventable = EM.watch(stream_err, StdErrHandler, options, stream_err) { |c| c.notify_readable = true } if options[:stderr_handler]
EM.watch(stream_out, StdOutHandler, options, stderr_eventable, stream_out, pid) { |c| c.notify_readable = true }
EM.watch(stream_out, StdOutHandler, options, stderr_eventable, stream_out, pid) do |c|
c.notify_readable = true
options[:target].method(options[:pid_handler]).call(pid) if options[:pid_handler]
end
EM.attach(stream_in, StdInHandler, options, stream_in) if options[:input]

# note that control returns to the caller, but the launched cmd continues
Expand Down
30 changes: 25 additions & 5 deletions spec/right_popen_spec.rb
Expand Up @@ -31,18 +31,21 @@ def initialize
end

attr_reader :output_text, :error_text, :status
attr_accessor :pid

def do_right_popen(command, env=nil, input=nil)
@timeout = EM::Timer.new(2) { puts "\n** Failed to run #{command.inspect}: Timeout"; EM.stop }
@output_text = ''
@error_text = ''
@status = nil
RightScale.popen3(:command => command,
@pid = nil
RightScale.popen3(:command => command,
:input => input,
:target => self,
:target => self,
:environment => env,
:stdout_handler => :on_read_stdout,
:stderr_handler => :on_read_stderr,
:stdout_handler => :on_read_stdout,
:stderr_handler => :on_read_stderr,
:pid_handler => :on_pid,
:exit_handler => :on_exit)
end

Expand All @@ -68,6 +71,11 @@ def on_read_stderr(data)
@error_text << data
end

def on_pid(pid)
raise "PID already set!" unless @pid.nil?
@pid = pid
end

def on_exit(status)
@last_iteration += 1
@timeout.cancel if @timeout
Expand All @@ -94,6 +102,7 @@ def on_exit(status)
runner.status.exitstatus.should == 0
runner.output_text.should == STANDARD_MESSAGE + "\n"
runner.error_text.should == ERROR_MESSAGE + "\n"
runner.pid.should > 0
end

it 'should return the right status' do
Expand All @@ -103,6 +112,7 @@ def on_exit(status)
runner.status.exitstatus.should == EXIT_STATUS
runner.output_text.should == ''
runner.error_text.should == ''
runner.pid.should > 0
end

it 'should preserve the integrity of stdout when stderr is unavailable' do
Expand All @@ -118,6 +128,7 @@ def on_exit(status)
end
runner.output_text.should == results
runner.error_text.should == ''
runner.pid.should > 0
end

it 'should preserve the integrity of stderr when stdout is unavailable' do
Expand All @@ -133,6 +144,7 @@ def on_exit(status)
end
runner.error_text.should == results
runner.output_text.should == ''
runner.pid.should > 0
end

it 'should preserve the integrity of stdout and stderr despite interleaving' do
Expand All @@ -153,17 +165,20 @@ def on_exit(status)
(results << "stderr #{i}\n") if 0 == i % 10
end
runner.error_text.should == results
runner.pid.should > 0
end

it 'should setup environment variables' do
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
runner = RightPopenSpec::Runner.new
runner.run_right_popen(command)
runner.status.exitstatus.should == 0
runner.output_text.should_not include('_test_')
runner.pid = nil
runner.run_right_popen(command, :__test__ => '42')
runner.status.exitstatus.should == 0
runner.output_text.should match(/^__test__=42$/)
runner.pid.should > 0
end

it 'should restore environment variables' do
Expand All @@ -177,6 +192,7 @@ def on_exit(status)
runner.output_text.should match(/^__test__=42$/)
ENV.each { |k, v| old_envs[k].should == v }
old_envs.each { |k, v| ENV[k].should == v }
runner.pid.should > 0
end

if is_windows?
Expand All @@ -188,6 +204,7 @@ def on_exit(status)
runner.run_right_popen(command, 'PATH' => "c:/bogus\\bin")
runner.status.exitstatus.should == 0
runner.output_text.should include('PATH=c:\\bogus\\bin;')
runner.pid.should > 0
end
else
it 'should allow running bash command lines starting with a built-in command' do
Expand All @@ -196,6 +213,7 @@ def on_exit(status)
runner.run_right_popen(command)
runner.status.exitstatus.should == 0
runner.output_text.should == "1\n2\n3\n4\n5\n"
runner.pid.should > 0
end
end

Expand All @@ -207,6 +225,7 @@ def on_exit(status)
runner.status.exitstatus.should == 0
runner.output_text.should == STANDARD_MESSAGE + "\n"
runner.error_text.should == ERROR_MESSAGE + "\n"
runner.pid.should > 0
end

it 'should pass input to child process' do
Expand All @@ -216,6 +235,7 @@ def on_exit(status)
runner.status.exitstatus.should == 0
runner.output_text.should == "43\n"
runner.error_text.should be_empty
runner.pid.should > 0
end

end

0 comments on commit 34a1f7b

Please sign in to comment.