summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNuo Yan <nuo@opscode.com>2011-03-01 15:42:44 -0800
committerNuo Yan <nuo@opscode.com>2011-03-01 15:44:06 -0800
commita189c67e0a7040b31058233f5e4247bb97442c4a (patch)
tree5513790ecdf154df5a125fa2513497d3850e5363
parent9da5010679e32d31d87925a7ee377c4a1fb9b38e (diff)
downloadchef-a189c67e0a7040b31058233f5e4247bb97442c4a.tar.gz
Fix CHEF-2080
-rw-r--r--chef/lib/chef/shell_out.rb96
-rw-r--r--chef/spec/unit/shell_out_spec.rb110
2 files changed, 103 insertions, 103 deletions
diff --git a/chef/lib/chef/shell_out.rb b/chef/lib/chef/shell_out.rb
index e7097b1e94..9ec477eb28 100644
--- a/chef/lib/chef/shell_out.rb
+++ b/chef/lib/chef/shell_out.rb
@@ -6,9 +6,9 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
-#
+#
# http://www.apache.org/licenses/LICENSE-2.0
-#
+#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -28,9 +28,9 @@ class Chef
# Provides a simplified interface to shelling out yet still collecting both
# standard out and standard error and providing full control over environment,
# working directory, uid, gid, etc.
- #
+ #
# No means for passing input to the subprocess is provided, nor is there any
- # way to inspect the output of the command as it is being read. If you need
+ # way to inspect the output of the command as it is being read. If you need
# to do that, you have to use popen4 (in Chef::Mixin::Command)
#
# === Platform Support
@@ -41,16 +41,16 @@ class Chef
READ_SIZE = 4096
DEFAULT_READ_TIMEOUT = 60
DEFAULT_ENVIRONMENT = {'LC_ALL' => 'C'}
-
+
attr_accessor :user, :group, :cwd, :valid_exit_codes
attr_reader :command, :umask, :environment
attr_writer :timeout
attr_reader :execution_time
-
+
attr_reader :stdout, :stderr, :status
-
+
attr_reader :stdin_pipe, :stdout_pipe, :stderr_pipe, :process_status_pipe
-
+
# === Arguments:
# Takes a single command, or a list of command fragments. These are used
# as arguments to Kernel.exec. See the Kernel.exec documentation for more
@@ -97,34 +97,34 @@ class Chef
def initialize(*command_args)
@stdout, @stderr = '', ''
@environment = DEFAULT_ENVIRONMENT
- @cwd = Dir.tmpdir
+ @cwd = nil
@valid_exit_codes = [0]
if command_args.last.is_a?(Hash)
parse_options(command_args.pop)
end
-
+
@command = command_args.size == 1 ? command_args.first : command_args
end
-
+
def umask=(new_umask)
@umask = (new_umask.respond_to?(:oct) ? new_umask.oct : new_umask.to_i) & 007777
end
-
+
def uid
return nil unless user
user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid
end
-
+
def gid
return nil unless group
group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid
end
-
+
def timeout
@timeout || DEFAULT_READ_TIMEOUT
end
-
+
# Creates a String showing the output of the command, including a banner
# showing the exact command executed. Used by +invalid!+ to show command
# results when the command exited with an unexpected status.
@@ -137,11 +137,11 @@ class Chef
msg << "Ran #{command} returned #{status.exitstatus}" if status
msg
end
-
+
def exitstatus
@status && @status.exitstatus
end
-
+
# Run the command, writing the command's standard out and standard error
# to +stdout+ and +stderr+, and saving its exit status object to +status+
# === Returns
@@ -156,13 +156,13 @@ class Chef
def run_command
Chef::Log.debug("sh(#{@command})")
@child_pid = fork_subprocess
-
+
configure_parent_process_file_descriptors
propagate_pre_exec_failure
-
+
@result = nil
@execution_time = 0
-
+
# Ruby 1.8.7 and 1.8.6 from mid 2009 try to allocate objects during GC
# when calling IO.select and IO#read. Some OS Vendors are not interested
# in updating their ruby packages (Apple, *cough*) and we *have to*
@@ -176,14 +176,14 @@ class Chef
raise Chef::Exceptions::CommandTimeout, "command timed out:\n#{format_for_exception}"
end
end
-
+
if ready && ready.first.include?(child_stdout)
read_stdout_to_buffer
end
if ready && ready.first.include?(child_stderr)
read_stderr_to_buffer
end
-
+
unless @status
# make one more pass to get the last of the output after the
# child process dies
@@ -205,8 +205,8 @@ class Chef
GC.enable
close_all_pipes
end
-
- # Checks the +exitstatus+ against the set of +valid_exit_codes+. If
+
+ # Checks the +exitstatus+ against the set of +valid_exit_codes+. If
# +exitstatus+ is not in the list of +valid_exit_codes+, calls +invalid!+,
# which raises an Exception.
# === Returns
@@ -231,15 +231,15 @@ class Chef
msg ||= "Command produced unexpected results"
raise Chef::Exceptions::ShellCommandFailed, msg + "\n" + format_for_exception
end
-
+
def inspect
"<#{self.class.name}##{object_id}: command: '#@command' process_status: #{@status.inspect} " +
- "stdout: '#{stdout.strip}' stderr: '#{stderr.strip}' child_pid: #{@child_pid.inspect} " +
+ "stdout: '#{stdout.strip}' stderr: '#{stderr.strip}' child_pid: #{@child_pid.inspect} " +
"environment: #{@environment.inspect} timeout: #{timeout} user: #@user group: #@group working_dir: #@cwd >"
end
private
-
+
def parse_options(opts)
opts.each do |option, setting|
case option.to_s
@@ -263,31 +263,31 @@ class Chef
end
end
end
-
+
def set_user
if user
Process.euid = uid
Process.uid = uid
end
- end
-
+ end
+
def set_group
if group
Process.egid = gid
Process.gid = gid
end
end
-
+
def set_environment
environment.each do |env_var,value|
ENV[env_var] = value
end
end
-
+
def set_umask
File.umask(umask) if umask
end
-
+
def set_cwd
Dir.chdir(cwd) if cwd
end
@@ -296,31 +296,31 @@ class Chef
@stdout_pipe, @stderr_pipe, @process_status_pipe = IO.pipe, IO.pipe, IO.pipe
@process_status_pipe.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
end
-
+
def child_stdout
@stdout_pipe[0]
end
-
+
def child_stderr
@stderr_pipe[0]
end
-
+
def child_process_status
@process_status_pipe[0]
end
-
+
def close_all_pipes
child_stdout.close unless child_stdout.closed?
child_stderr.close unless child_stderr.closed?
child_process_status.close unless child_process_status.closed?
end
-
+
# replace stdout, and stderr with pipes to the parent, and close the
# reader side of the error marshaling side channel. Close STDIN so when we
# exec, the new program will know it's never getting input ever.
def configure_subprocess_file_descriptors
process_status_pipe.first.close
-
+
# HACK: for some reason, just STDIN.close isn't good enough when running
# under ruby 1.9.2, so make it good enough:
stdin_reader, stdin_writer = IO.pipe
@@ -338,7 +338,7 @@ class Chef
STDOUT.sync = STDERR.sync = true
end
-
+
def configure_parent_process_file_descriptors
# Close the sides of the pipes we don't care about
stdout_pipe.last.close
@@ -350,7 +350,7 @@ class Chef
true
end
-
+
# Some patch levels of ruby in wide use (in particular the ruby 1.8.6 on OSX)
# segfault when you IO.select a pipe that's reached eof. Weak sauce.
def open_pipes
@@ -365,7 +365,7 @@ class Chef
rescue EOFError
open_pipes.delete_at(0)
end
-
+
def read_stderr_to_buffer
while chunk = child_stderr.read_nonblock(READ_SIZE)
@stderr << chunk
@@ -374,13 +374,13 @@ class Chef
rescue EOFError
open_pipes.delete_at(1)
end
-
+
def fork_subprocess
initialize_ipc
-
+
fork do
configure_subprocess_file_descriptors
-
+
set_user
set_group
set_environment
@@ -389,7 +389,7 @@ class Chef
begin
command.kind_of?(Array) ? exec(*command) : exec(command)
-
+
raise 'forty-two' # Should never get here
rescue Exception => e
Marshal.dump(e, process_status_pipe.last)
@@ -399,7 +399,7 @@ class Chef
exit!
end
end
-
+
# Attempt to get a Marshaled error from the side-channel.
# If it's there, un-marshal it and raise. If it's not there,
# assume everything went well.
@@ -413,6 +413,6 @@ class Chef
child_process_status.close
end
end
-
+
end
end
diff --git a/chef/spec/unit/shell_out_spec.rb b/chef/spec/unit/shell_out_spec.rb
index a1a71889af..65235008be 100644
--- a/chef/spec/unit/shell_out_spec.rb
+++ b/chef/spec/unit/shell_out_spec.rb
@@ -4,140 +4,140 @@ describe Chef::ShellOut do
before do
@shell_cmd = Chef::ShellOut.new("apt-get install chef")
end
-
+
it "has a command" do
@shell_cmd.command.should == "apt-get install chef"
end
-
- it "has a working dir" do
- @shell_cmd.cwd.should == Dir.tmpdir
+
+ it "by default does not haave a working dir" do
+ @shell_cmd.cwd.should == nil
end
-
+
it "has a user to run the command as" do
@shell_cmd.user.should be_nil
end
-
+
it "sets the user to run the command as" do
@shell_cmd.user = 'root'
@shell_cmd.user.should == 'root'
end
-
+
it "has a group to run the command as" do
@shell_cmd.group.should be_nil
end
-
+
it "sets the group to run the command as" do
@shell_cmd.group = 'wheel'
@shell_cmd.group.should == 'wheel'
end
-
+
it "has a set of environment variables to set before running the command" do
@shell_cmd.environment.should == {"LC_ALL" => "C"}
end
-
+
it "has a umask" do
@shell_cmd.umask.should be_nil
end
-
+
it "sets the umask using an octal integer" do
@shell_cmd.umask = 007777
@shell_cmd.umask.should == 007777
end
-
+
it "sets the umask using a decimal integer" do
@shell_cmd.umask = 2925
@shell_cmd.umask.should == 005555
end
-
+
it "sets the umask using a string representation of an integer" do
@shell_cmd.umask = '7777'
@shell_cmd.umask.should == 007777
end
-
+
it "returns the user-supplied uid when present" do
@shell_cmd.user = 0
@shell_cmd.uid.should == 0
end
-
+
it "computes the uid of the user when a string/symbolic username is given" do
@shell_cmd.user = Etc.getlogin
@shell_cmd.uid.should == Etc.getpwuid.uid
end
-
+
it "returns the user-supplied gid when present" do
@shell_cmd.group = 0
@shell_cmd.gid.should == 0
end
-
+
it "computes the gid of the user when a string/symbolic groupname is given" do
a_group = Etc.getgrent
@shell_cmd.group = a_group.name
@shell_cmd.gid.should == a_group.gid
end
-
+
it "has a timeout defaulting to 60 seconds" do
Chef::ShellOut.new('foo').timeout.should == 60
end
-
+
it "sets the read timeout" do
@shell_cmd.timeout = 10
@shell_cmd.timeout.should == 10
end
-
+
it "has a list of valid exit codes which is just 0 by default" do
@shell_cmd.valid_exit_codes.should == [0]
end
-
+
it "sets the list of valid exit codes" do
@shell_cmd.valid_exit_codes = [0,23,42]
@shell_cmd.valid_exit_codes.should == [0,23,42]
end
-
+
context "when initialized with a hash of options" do
before do
@opts = { :cwd => '/tmp', :user => 'toor', :group => 'wheel', :umask => '2222',
:timeout => 5, :environment => {'RUBY_OPTS' => '-w'}, :returns => [0,1,42]}
@shell_cmd = Chef::ShellOut.new("brew install couchdb", @opts)
end
-
+
it "sets the working dir as specified in the options" do
@shell_cmd.cwd.should == '/tmp'
end
-
+
it "sets the user as specified in the options" do
@shell_cmd.user.should == 'toor'
end
-
+
it "sets the group as specified in the options" do
@shell_cmd.group.should == 'wheel'
end
-
+
it "sets the umask as specified in the options" do
@shell_cmd.umask.should == 002222
end
-
+
it "sets the timout as specified in the options" do
@shell_cmd.timeout.should == 5
end
-
+
it "merges the environment with the default environment settings" do
@shell_cmd.environment.should == {'LC_ALL' => 'C', 'RUBY_OPTS' => '-w'}
end
-
+
it "also accepts :env to set the enviroment for brevity's sake" do
@shell_cmd = Chef::ShellOut.new("brew install couchdb", :env => {'RUBY_OPTS'=>'-w'})
@shell_cmd.environment.should == {'LC_ALL' => 'C', 'RUBY_OPTS' => '-w'}
end
-
+
it "does not set any environment settings when given :environment => nil" do
@shell_cmd = Chef::ShellOut.new("brew install couchdb", :environment => nil)
@shell_cmd.environment.should == {}
end
-
+
it "sets the list of acceptable return values" do
@shell_cmd.valid_exit_codes.should == [0,1,42]
end
-
+
it "raises an error when given an invalid option" do
klass = Chef::Exceptions::InvalidCommandOption
msg = "option ':frab' is not a valid option for Chef::ShellOut"
@@ -151,34 +151,34 @@ describe Chef::ShellOut do
cmd.stdout.should == "/bin\n"
end
end
-
+
context "when initialized with an array of command+args and an options hash" do
before do
@opts = {:cwd => '/tmp', :user => 'nobody'}
@shell_cmd = Chef::ShellOut.new('ruby', '-e', %q{'puts "hello"'}, @opts)
end
-
+
it "sets the command to the array of command and args" do
@shell_cmd.command.should == ['ruby', '-e', %q{'puts "hello"'}]
end
-
+
it "evaluates the options" do
@shell_cmd.cwd.should == '/tmp'
@shell_cmd.user.should == 'nobody'
end
end
-
+
context "when initialized with an array of command+args and no options" do
before do
@shell_cmd = Chef::ShellOut.new('ruby', '-e', %q{'puts "hello"'})
end
-
+
it "sets the command to the array of command+args" do
@shell_cmd.command.should == ['ruby', '-e', %q{'puts "hello"'}]
end
-
+
end
-
+
describe "handling various subprocess behaviors" do
it "collects all of STDOUT and STDERR" do
twotime = %q{ruby -e 'STDERR.puts :hello; STDOUT.puts :world'}
@@ -187,35 +187,35 @@ describe Chef::ShellOut do
cmd.stderr.should == "hello\n"
cmd.stdout.should == "world\n"
end
-
+
it "collects the exit status of the command" do
cmd = Chef::ShellOut.new('ruby -e "exit 0"')
status = cmd.run_command.status
status.should be_a_kind_of(Process::Status)
status.exitstatus.should == 0
end
-
+
it "does not hang if a process forks but does not close stdout and stderr" do
evil_forker="exit if fork; 10.times { sleep 1}"
cmd = Chef::ShellOut.new("ruby -e '#{evil_forker}'")
-
+
lambda {Timeout.timeout(2) do
cmd.run_command
end}.should_not raise_error
end
-
+
it "times out when a process takes longer than the specified timeout" do
cmd = Chef::ShellOut.new("sleep 2", :timeout => 0.1)
lambda {cmd.run_command}.should raise_error(Chef::Exceptions::CommandTimeout)
end
-
+
it "reads all of the output when the subprocess produces more than $buffersize of output" do
chatty = %q|ruby -e "print('X' * 16 * 1024); print( '.' * 1024)"|
cmd = Chef::ShellOut.new(chatty)
cmd.run_command
cmd.stdout.should match(/X{16384}\.{1024}/)
end
-
+
it "returns empty strings from commands that have no output" do
cmd = Chef::ShellOut.new(%q{ruby -e 'exit 0'})
cmd.run_command
@@ -245,40 +245,40 @@ describe Chef::ShellOut do
cmd.stdout.should == ('f' * 20_000) + "\n" + ('f' * 20_000) + "\n"
cmd.stderr.should == ('u' * 20_000) + "\n" + ('u' * 20_000) + "\n"
end
-
+
it "doesn't hang or lose output when a process writes, pauses, then continues writing" do
stop_and_go = %q{ruby -e 'puts "before";sleep 0.5;puts"after"'}
cmd = Chef::ShellOut.new(stop_and_go)
cmd.run_command
cmd.stdout.should == "before\nafter\n"
end
-
+
it "doesn't hang or lose output when a process pauses before writing" do
late_arrival = %q{ruby -e 'sleep 0.5;puts "missed_the_bus"'}
cmd = Chef::ShellOut.new(late_arrival)
cmd.run_command
cmd.stdout.should == "missed_the_bus\n"
end
-
+
it "uses the C locale by default" do
cmd = Chef::ShellOut.new('echo $LC_ALL')
cmd.run_command
cmd.stdout.strip.should == 'C'
end
-
+
it "does not set any locale when the user gives LC_ALL => nil" do
# kinda janky
cmd = Chef::ShellOut.new('echo $LC_ALL', :environment => {"LC_ALL" => nil})
cmd.run_command
cmd.stdout.strip.should == ENV['LC_ALL'].to_s.strip
end
-
+
it "uses the requested locale" do
cmd = Chef::ShellOut.new('echo $LC_ALL', :environment => {"LC_ALL" => 'es'})
cmd.run_command
cmd.stdout.strip.should == 'es'
end
-
+
it "recovers the error message when exec fails" do
cmd = Chef::ShellOut.new("fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu")
lambda {cmd.run_command}.should raise_error(Errno::ENOENT)
@@ -290,7 +290,7 @@ describe Chef::ShellOut do
cmd.stdout.should == "true"
end
end
-
+
it "formats itself for exception messages" do
cmd = Chef::ShellOut.new %q{ruby -e 'STDERR.puts "msg_in_stderr"; puts "msg_in_stdout"'}
cmd.run_command
@@ -299,7 +299,7 @@ describe Chef::ShellOut do
cmd.format_for_exception.split("\n")[2].should == %q{STDERR: msg_in_stderr}
cmd.format_for_exception.split("\n")[3].should == %q{---- End output of ruby -e 'STDERR.puts "msg_in_stderr"; puts "msg_in_stdout"' ----}
end
-
+
it "raises a InvalidCommandResult error if the exitstatus is an unexpected value" do
cmd = Chef::ShellOut.new('ruby -e "exit 2"')
cmd.run_command
@@ -311,7 +311,7 @@ describe Chef::ShellOut do
cmd.run_command
lambda {cmd.error!}.should_not raise_error
end
-
+
it "includes output with exceptions from #error!" do
cmd = Chef::ShellOut.new('ruby -e "exit 2"')
cmd.run_command
@@ -321,7 +321,7 @@ describe Chef::ShellOut do
e.message.should match(Regexp.escape(cmd.format_for_exception))
end
end
-
+
it "errors out when told the result is invalid" do
cmd = Chef::ShellOut.new('ruby -e "exit 0"')
cmd.run_command