diff options
author | Nuo Yan <nuo@opscode.com> | 2011-03-01 15:42:44 -0800 |
---|---|---|
committer | Nuo Yan <nuo@opscode.com> | 2011-03-01 15:44:06 -0800 |
commit | a189c67e0a7040b31058233f5e4247bb97442c4a (patch) | |
tree | 5513790ecdf154df5a125fa2513497d3850e5363 | |
parent | 9da5010679e32d31d87925a7ee377c4a1fb9b38e (diff) | |
download | chef-a189c67e0a7040b31058233f5e4247bb97442c4a.tar.gz |
Fix CHEF-2080
-rw-r--r-- | chef/lib/chef/shell_out.rb | 96 | ||||
-rw-r--r-- | chef/spec/unit/shell_out_spec.rb | 110 |
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 |