diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb index 220cc465e..74f4da37b 100644 --- a/lib/puppet/util/execution.rb +++ b/lib/puppet/util/execution.rb @@ -1,244 +1,243 @@ module Puppet require 'rbconfig' # A command failed to execute. require 'puppet/error' class ExecutionFailure < Puppet::Error end module Util::Execution # Execute the provided command with STDIN connected to a pipe, yielding the # pipe object. That allows data to be fed to that subprocess. # # The command can be a simple string, which is executed as-is, or an Array, # which is treated as a set of command arguments to pass through.# # # In all cases this is passed directly to the shell, and STDOUT and STDERR # are connected together during execution. def self.execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '#{command}'" else Puppet.debug "Executing '#{command}'" end # Paste together an array with spaces. We used to paste directly # together, no spaces, which made for odd invocations; the user had to # include whitespace between arguments. # # Having two spaces is really not a big drama, since this passes to the # shell anyhow, while no spaces makes for a small developer cost every # time this is invoked. --daniel 2012-02-13 command_str = command.respond_to?(:join) ? command.join(' ') : command output = open("| #{command_str} 2>&1") do |pipe| yield pipe end if failonfail unless $CHILD_STATUS == 0 raise ExecutionFailure, output end end output end def self.execfail(command, exception) output = execute(command) return output rescue ExecutionFailure raise exception, output end # Execute the desired command, and return the status and output. # def execute(command, arguments) # [arguments] a Hash optionally containing any of the following keys: # :failonfail (default true) -- if this value is set to true, then this method will raise an error if the # command is not executed successfully. # :uid (default nil) -- the user id of the user that the process should be run as # :gid (default nil) -- the group id of the group that the process should be run as # :combine (default true) -- sets whether or not to combine stdout/stderr in the output # :stdinfile (default nil) -- sets a file that can be used for stdin. Passing a string for stdin is not currently # supported. # :squelch (default false) -- if true, ignore stdout / stderr completely # :override_locale (default true) -- by default (and if this option is set to true), we will temporarily override # the user/system locale to "C" (via environment variables LANG and LC_*) while we are executing the command. # This ensures that the output of the command will be formatted consistently, making it predictable for parsing. # Passing in a value of false for this option will allow the command to be executed using the user/system locale. # :custom_environment (default {}) -- a hash of key/value pairs to set as environment variables for the duration # of the command def self.execute(command, arguments = {}) - # specifying these here rather than in the method signature to allow callers to pass in a partial # set of overrides without affecting the default values for options that they don't pass in default_arguments = { :failonfail => true, :uid => nil, :gid => nil, :combine => true, :stdinfile => nil, :squelch => false, :override_locale => true, :custom_environment => {}, } arguments = default_arguments.merge(arguments) if command.is_a?(Array) command = command.flatten.map(&:to_s) str = command.join(" ") elsif command.is_a?(String) str = command end if respond_to? :debug debug "Executing '#{str}'" else Puppet.debug "Executing '#{str}'" end null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' stdin = File.open(arguments[:stdinfile] || null_file, 'r') stdout = arguments[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet') stderr = arguments[:combine] ? stdout : File.open(null_file, 'w') exec_args = [command, arguments, stdin, stdout, stderr] if execution_stub = Puppet::Util::ExecutionStub.current_value return execution_stub.call(*exec_args) elsif Puppet.features.posix? child_pid = execute_posix(*exec_args) exit_status = Process.waitpid2(child_pid).last.exitstatus elsif Puppet.features.microsoft_windows? process_info = execute_windows(*exec_args) begin exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle) ensure Process.CloseHandle(process_info.process_handle) Process.CloseHandle(process_info.thread_handle) end end [stdin, stdout, stderr].each {|io| io.close rescue nil} # read output in if required unless arguments[:squelch] output = wait_for_output(stdout) Puppet.warning "Could not get output" unless output end if arguments[:failonfail] and exit_status != 0 raise ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output}" end output end # get the path to the ruby executable (available via Config object, even if # it's not in the PATH... so this is slightly safer than just using # Puppet::Util.which) def self.ruby_path() File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']). sub(/.*\s.*/m, '"\&"') end # Because some modules provide their own version of this method. class << self alias util_execute execute end # this is private method, see call to private_class_method after method definition def self.execute_posix(command, arguments, stdin, stdout, stderr) child_pid = Puppet::Util.safe_posix_fork(stdin, stdout, stderr) do # We can't just call Array(command), and rely on it returning # things like ['foo'], when passed ['foo'], because # Array(command) will call command.to_a internally, which when # given a string can end up doing Very Bad Things(TM), such as # turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"] command = [command].flatten Process.setsid begin Puppet::Util::SUIDManager.change_privileges(arguments[:uid], arguments[:gid], true) # if the caller has requested that we override locale environment variables, if (arguments[:override_locale]) then # loop over them and clear them Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) } # set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in # a forked process. ENV['LANG'] = 'C' ENV['LC_ALL'] = 'C' end # unset all of the user-related environment variables so that different methods of starting puppet # (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side # effects relating to user / home dir environment vars. # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in # a forked process. Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) } arguments[:custom_environment] ||= {} Puppet::Util.withenv(arguments[:custom_environment]) do Kernel.exec(*command) end rescue => detail Puppet.log_exception(detail, "Could not execute posix command: #{detail}") exit!(1) end end child_pid end private_class_method :execute_posix # this is private method, see call to private_class_method after method definition def self.execute_windows(command, arguments, stdin, stdout, stderr) command = command.map do |part| part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part end.join(" ") if command.is_a?(Array) arguments[:custom_environment] ||= {} Puppet::Util.withenv(arguments[:custom_environment]) do Puppet::Util::Windows::Process.execute(command, arguments, stdin, stdout, stderr) end end private_class_method :execute_windows # this is private method, see call to private_class_method after method definition def self.wait_for_output(stdout) # Make sure the file's actually been written. This is basically a race # condition, and is probably a horrible way to handle it, but, well, oh # well. # (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry # about a race condition because all of the places that we call this from are preceded by a call to "waitpid2", # meaning that the processes responsible for writing the file have completed before we get here.) 2.times do |try| if File.exists?(stdout.path) output = stdout.open.read stdout.close(true) return output else time_to_sleep = try / 2.0 Puppet.warning "Waiting for output; will sleep #{time_to_sleep} seconds" sleep(time_to_sleep) end end nil end private_class_method :wait_for_output end end diff --git a/spec/unit/provider/service/base_spec.rb b/spec/unit/provider/service/base_spec.rb index 9522fd7f8..03a33e259 100755 --- a/spec/unit/provider/service/base_spec.rb +++ b/spec/unit/provider/service/base_spec.rb @@ -1,77 +1,77 @@ #!/usr/bin/env rspec require 'spec_helper' require 'rbconfig' require 'fileutils' provider_class = Puppet::Type.type(:service).provider(:init) describe "base service provider" do include PuppetSpec::Files let :type do Puppet::Type.type(:service) end let :provider do type.provider(:base) end subject { provider } context "basic operations" do # Cross-platform file interactions. Fun times. Ruby = File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["RUBY_INSTALL_NAME"] + RbConfig::CONFIG["EXEEXT"]) - Start = "#{Ruby} -rfileutils -e 'FileUtils.touch(ARGV[0])'" - Status = "#{Ruby} -e 'exit File.file?(ARGV[0])'" - Stop = "#{Ruby} -e 'File.exist?(ARGV[0]) and File.unlink(ARGV[0])'" + Start = [Ruby, '-rfileutils', '-e', 'FileUtils.touch(ARGV[0])'] + Status = [Ruby, '-e' 'exit File.file?(ARGV[0])'] + Stop = [Ruby, '-e', 'File.exist?(ARGV[0]) and File.unlink(ARGV[0])'] let :flag do tmpfile('base-service-test') end subject do type.new(:name => "test", :provider => :base, - :start => "#{Start} #{flag}", - :status => "#{Status} #{flag}", - :stop => "#{Stop} #{flag}" + :start => Start + [flag], + :status => Status + [flag], + :stop => Stop + [flag] ).provider end before :each do File.unlink(flag) if File.exist?(flag) end it { should be } it "should invoke the start command if not running" do File.should_not be_file flag subject.start File.should be_file flag end it "should be stopped before being started" do subject.status.should == :stopped end it "should be running after being started" do subject.start subject.status.should == :running end it "should invoke the stop command when asked" do subject.start subject.status.should == :running subject.stop subject.status.should == :stopped File.should_not be_file flag end it "should start again even if already running" do subject.start subject.expects(:ucommand).with(:start) subject.start end it "should stop again even if already stopped" do subject.stop subject.expects(:ucommand).with(:stop) subject.stop end end end