mixlib-shellout-1.3.0/0000755000004100000410000000000012261203217014660 5ustar www-datawww-datamixlib-shellout-1.3.0/lib/0000755000004100000410000000000012261203217015426 5ustar www-datawww-datamixlib-shellout-1.3.0/lib/mixlib/0000755000004100000410000000000012261203217016712 5ustar www-datawww-datamixlib-shellout-1.3.0/lib/mixlib/shellout.rb0000644000004100000410000002665212261203217021111 0ustar www-datawww-data#-- # Author:: Daniel DeLeo () # Copyright:: Copyright (c) 2010, 2011 Opscode, Inc. # License:: Apache License, Version 2.0 # # 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. # See the License for the specific language governing permissions and # limitations under the License. # require 'etc' require 'tmpdir' require 'fcntl' require 'mixlib/shellout/exceptions' module Mixlib class ShellOut READ_WAIT_TIME = 0.01 READ_SIZE = 4096 DEFAULT_READ_TIMEOUT = 600 DEFAULT_ENVIRONMENT = {'LC_ALL' => 'C'} if RUBY_PLATFORM =~ /mswin|mingw32|windows/ require 'mixlib/shellout/windows' include ShellOut::Windows else require 'mixlib/shellout/unix' include ShellOut::Unix end # User the command will run as. Normally set via options passed to new attr_accessor :user attr_accessor :domain attr_accessor :password attr_accessor :with_logon # Group the command will run as. Normally set via options passed to new attr_accessor :group # Working directory for the subprocess. Normally set via options to new attr_accessor :cwd # An Array of acceptable exit codes. #error! uses this list to determine if # the command was successful. Normally set via options to new attr_accessor :valid_exit_codes # When live_stream is set, stdout of the subprocess will be copied to it as # the subprocess is running. For example, if live_stream is set to STDOUT, # the command's output will be echoed to STDOUT. attr_accessor :live_stream # ShellOut will push data from :input down the stdin of the subprocss. # Normally set via options passed to new. # Default: nil attr_accessor :input # If a logger is set, ShellOut will log a message before it executes the # command. attr_accessor :logger # The log level at which ShellOut should log. attr_accessor :log_level # A string which will be prepended to the log message. attr_accessor :log_tag # The command to be executed. attr_reader :command # The umask that will be set for the subcommand. attr_reader :umask # Environment variables that will be set for the subcommand. Refer to the # documentation of new to understand how ShellOut interprets this. attr_reader :environment # The maximum time this command is allowed to run. Usually set via options # to new attr_writer :timeout # The amount of time the subcommand took to execute attr_reader :execution_time # Data written to stdout by the subprocess attr_reader :stdout # Data written to stderr by the subprocess attr_reader :stderr # A Process::Status (or ducktype) object collected when the subprocess is # reaped. attr_reader :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 # explanation of how arguments are evaluated. The last argument can be an # options Hash. # === Options: # If the last argument is a Hash, it is removed from the list of args passed # to exec and used as an options hash. The following options are available: # * +user+: the user the commmand should run as. if an integer is given, it is # used as a uid. A string is treated as a username and resolved to a uid # with Etc.getpwnam # * +group+: the group the command should run as. works similarly to +user+ # * +cwd+: the directory to chdir to before running the command # * +umask+: a umask to set before running the command. If given as an Integer, # be sure to use two leading zeros so it's parsed as Octal. A string will # be treated as an octal integer # * +returns+: one or more Integer values to use as valid exit codes for the # subprocess. This only has an effect if you call +error!+ after # +run_command+. # * +environment+: a Hash of environment variables to set before the command # is run. By default, the environment will *always* be set to 'LC_ALL' => 'C' # to prevent issues with multibyte characters in Ruby 1.8. To avoid this, # use :environment => nil for *no* extra environment settings, or # :environment => {'LC_ALL'=>nil, ...} to set other environment settings # without changing the locale. # * +timeout+: a Numeric value for the number of seconds to wait on the # child process before raising an Exception. This is calculated as the # total amount of time that ShellOut waited on the child process without # receiving any output (i.e., IO.select returned nil). Default is 60 # seconds. Note: the stdlib Timeout library is not used. # === Examples: # Invoke find(1) to search for .rb files: # find = Mixlib::ShellOut.new("find . -name '*.rb'") # find.run_command # # If all went well, the results are on +stdout+ # puts find.stdout # # find(1) prints diagnostic info to STDERR: # puts "error messages" + find.stderr # # Raise an exception if it didn't exit with 0 # find.error! # Run a command as the +www+ user with no extra ENV settings from +/tmp+ # cmd = Mixlib::ShellOut.new("apachectl", "start", :user => 'www', :env => nil, :cwd => '/tmp') # cmd.run_command # etc. def initialize(*command_args) @stdout, @stderr = '', '' @live_stream = nil @input = nil @log_level = :debug @log_tag = nil @environment = DEFAULT_ENVIRONMENT @cwd = nil @valid_exit_codes = [0] @terminate_reason = nil if command_args.last.is_a?(Hash) parse_options(command_args.pop) end @command = command_args.size == 1 ? command_args.first : command_args end # Set the umask that the subprocess will have. If given as a string, it # will be converted to an integer by String#oct. def umask=(new_umask) @umask = (new_umask.respond_to?(:oct) ? new_umask.oct : new_umask.to_i) & 007777 end # The uid that the subprocess will switch to. If the user attribute was # given as a username, it is converted to a uid by Etc.getpwnam def uid return nil unless user user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid end # The gid that the subprocess will switch to. If the group attribute is # given as a group name, it is converted to a gid by Etc.getgrnam 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. def format_for_exception msg = "" msg << "#{@terminate_reason}\n" if @terminate_reason msg << "---- Begin output of #{command} ----\n" msg << "STDOUT: #{stdout.strip}\n" msg << "STDERR: #{stderr.strip}\n" msg << "---- End output of #{command} ----\n" msg << "Ran #{command} returned #{status.exitstatus}" if status msg end # The exit status of the subprocess. Will be nil if the command is still # running or died without setting an exit status (e.g., terminated by # `kill -9`). 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 # returns +self+; +stdout+, +stderr+, +status+, and +exitstatus+ will be # populated with results of the command # === Raises # * Errno::EACCES when you are not privileged to execute the command # * Errno::ENOENT when the command is not available on the system (or not # in the current $PATH) # * CommandTimeout when the command does not complete # within +timeout+ seconds (default: 600s) def run_command if logger log_message = (log_tag.nil? ? "" : "#@log_tag ") << "sh(#@command)" logger.send(log_level, log_message) end super end # 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 # nil::: always returns nil when it does not raise # === Raises # ::ShellCommandFailed::: via +invalid!+ def error! unless Array(valid_exit_codes).include?(exitstatus) invalid!("Expected process to exit with #{valid_exit_codes.inspect}, but received '#{exitstatus}'") end end # Raises a ShellCommandFailed exception, appending the # command's stdout, stderr, and exitstatus to the exception message. # === Arguments # +msg+: A String to use as the basis of the exception message. The # default explanation is very generic, providing a more informative message # is highly encouraged. # === Raises # ShellCommandFailed always def invalid!(msg=nil) msg ||= "Command produced unexpected results" raise 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} " + "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 when 'cwd' self.cwd = setting when 'domain' self.domain = setting when 'password' self.password = setting when 'user' self.user = setting self.with_logon = setting when 'group' self.group = setting when 'umask' self.umask = setting when 'timeout' self.timeout = setting when 'returns' self.valid_exit_codes = Array(setting) when 'live_stream' self.live_stream = setting when 'input' self.input = setting when 'logger' self.logger = setting when 'log_level' self.log_level = setting when 'log_tag' self.log_tag = setting when 'environment', 'env' # Set the LC_ALL from the parent process if the user wanted # to use the default. if setting && setting.has_key?("LC_ALL") && setting['LC_ALL'].nil? setting['LC_ALL'] = ENV['LC_ALL'] end # passing :environment => nil means don't set any new ENV vars @environment = setting.nil? ? {} : @environment.dup.merge!(setting) else raise InvalidCommandOption, "option '#{option.inspect}' is not a valid option for #{self.class.name}" end end validate_options(opts) end def validate_options(opts) super end end end mixlib-shellout-1.3.0/lib/mixlib/shellout/0000755000004100000410000000000012261203217020551 5ustar www-datawww-datamixlib-shellout-1.3.0/lib/mixlib/shellout/windows/0000755000004100000410000000000012261203217022243 5ustar www-datawww-datamixlib-shellout-1.3.0/lib/mixlib/shellout/windows/core_ext.rb0000644000004100000410000002707212261203217024410 0ustar www-datawww-data#-- # Author:: Daniel DeLeo () # Author:: John Keiser () # Copyright:: Copyright (c) 2011, 2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # 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. # See the License for the specific language governing permissions and # limitations under the License. # require 'win32/process' # Add new constants for Logon module Process::Constants LOGON32_LOGON_INTERACTIVE = 0x00000002 LOGON32_PROVIDER_DEFAULT = 0x00000000 UOI_NAME = 0x00000002 end # Define the functions needed to check with Service windows station module Process::Functions module FFI::Library # Wrapper method for attach_function + private def attach_pfunc(*args) attach_function(*args) private args[0] end end extend FFI::Library ffi_lib :advapi32 attach_pfunc :LogonUserW, [:buffer_in, :buffer_in, :buffer_in, :ulong, :ulong, :pointer], :bool attach_pfunc :CreateProcessAsUserW, [:ulong, :buffer_in, :buffer_in, :pointer, :pointer, :bool, :ulong, :buffer_in, :buffer_in, :pointer, :pointer], :bool ffi_lib :user32 attach_pfunc :GetProcessWindowStation, [], :ulong attach_pfunc :GetUserObjectInformationA, [:ulong, :uint, :buffer_out, :ulong, :pointer], :bool end # Override Process.create to check for running in the Service window station and doing # a full logon with LogonUser, instead of a CreateProcessWithLogon module Process include Process::Constants include Process::Structs def create(args) unless args.kind_of?(Hash) raise TypeError, 'hash keyword arguments expected' end valid_keys = %w[ app_name command_line inherit creation_flags cwd environment startup_info thread_inherit process_inherit close_handles with_logon domain password ] valid_si_keys = %w[ startf_flags desktop title x y x_size y_size x_count_chars y_count_chars fill_attribute sw_flags stdin stdout stderr ] # Set default values hash = { 'app_name' => nil, 'creation_flags' => 0, 'close_handles' => true } # Validate the keys, and convert symbols and case to lowercase strings. args.each{ |key, val| key = key.to_s.downcase unless valid_keys.include?(key) raise ArgumentError, "invalid key '#{key}'" end hash[key] = val } si_hash = {} # If the startup_info key is present, validate its subkeys if hash['startup_info'] hash['startup_info'].each{ |key, val| key = key.to_s.downcase unless valid_si_keys.include?(key) raise ArgumentError, "invalid startup_info key '#{key}'" end si_hash[key] = val } end # The +command_line+ key is mandatory unless the +app_name+ key # is specified. unless hash['command_line'] if hash['app_name'] hash['command_line'] = hash['app_name'] hash['app_name'] = nil else raise ArgumentError, 'command_line or app_name must be specified' end end env = nil # The env string should be passed as a string of ';' separated paths. if hash['environment'] env = hash['environment'] unless env.respond_to?(:join) env = hash['environment'].split(File::PATH_SEPARATOR) end env = env.map{ |e| e + 0.chr }.join('') + 0.chr env.to_wide_string! if hash['with_logon'] end # Process SECURITY_ATTRIBUTE structure process_security = nil if hash['process_inherit'] process_security = SECURITY_ATTRIBUTES.new process_security[:nLength] = 12 process_security[:bInheritHandle] = true end # Thread SECURITY_ATTRIBUTE structure thread_security = nil if hash['thread_inherit'] thread_security = SECURITY_ATTRIBUTES.new thread_security[:nLength] = 12 thread_security[:bInheritHandle] = true end # Automatically handle stdin, stdout and stderr as either IO objects # or file descriptors. This won't work for StringIO, however. It also # will not work on JRuby because of the way it handles internal file # descriptors. # ['stdin', 'stdout', 'stderr'].each{ |io| if si_hash[io] if si_hash[io].respond_to?(:fileno) handle = get_osfhandle(si_hash[io].fileno) else handle = get_osfhandle(si_hash[io]) end if handle == INVALID_HANDLE_VALUE ptr = FFI::MemoryPointer.new(:int) if windows_version >= 6 && get_errno(ptr) == 0 errno = ptr.read_int else errno = FFI.errno end raise SystemCallError.new("get_osfhandle", errno) end # Most implementations of Ruby on Windows create inheritable # handles by default, but some do not. RF bug #26988. bool = SetHandleInformation( handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT ) raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool si_hash[io] = handle si_hash['startf_flags'] ||= 0 si_hash['startf_flags'] |= STARTF_USESTDHANDLES hash['inherit'] = true end } procinfo = PROCESS_INFORMATION.new startinfo = STARTUPINFO.new unless si_hash.empty? startinfo[:cb] = startinfo.size startinfo[:lpDesktop] = si_hash['desktop'] if si_hash['desktop'] startinfo[:lpTitle] = si_hash['title'] if si_hash['title'] startinfo[:dwX] = si_hash['x'] if si_hash['x'] startinfo[:dwY] = si_hash['y'] if si_hash['y'] startinfo[:dwXSize] = si_hash['x_size'] if si_hash['x_size'] startinfo[:dwYSize] = si_hash['y_size'] if si_hash['y_size'] startinfo[:dwXCountChars] = si_hash['x_count_chars'] if si_hash['x_count_chars'] startinfo[:dwYCountChars] = si_hash['y_count_chars'] if si_hash['y_count_chars'] startinfo[:dwFillAttribute] = si_hash['fill_attribute'] if si_hash['fill_attribute'] startinfo[:dwFlags] = si_hash['startf_flags'] if si_hash['startf_flags'] startinfo[:wShowWindow] = si_hash['sw_flags'] if si_hash['sw_flags'] startinfo[:cbReserved2] = 0 startinfo[:hStdInput] = si_hash['stdin'] if si_hash['stdin'] startinfo[:hStdOutput] = si_hash['stdout'] if si_hash['stdout'] startinfo[:hStdError] = si_hash['stderr'] if si_hash['stderr'] end app = nil cmd = nil # Convert strings to wide character strings if present if hash['app_name'] app = hash['app_name'].to_wide_string end if hash['command_line'] cmd = hash['command_line'].to_wide_string end if hash['cwd'] cwd = hash['cwd'].to_wide_string end inherit = hash['inherit'] || false if hash['with_logon'] logon = hash['with_logon'].to_wide_string if hash['password'] passwd = hash['password'].to_wide_string else raise ArgumentError, 'password must be specified if with_logon is used' end if hash['domain'] domain = hash['domain'].to_wide_string end hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT winsta_name = FFI::MemoryPointer.new(:char, 256) return_size = FFI::MemoryPointer.new(:ulong) bool = GetUserObjectInformationA( GetProcessWindowStation(), # Window station handle UOI_NAME, # Information to get winsta_name, # Buffer to receive information winsta_name.size, # Size of buffer return_size # Size filled into buffer ) unless bool raise SystemCallError.new("GetUserObjectInformationA", FFI.errno) end winsta_name = winsta_name.read_string(return_size.read_ulong) # If running in the service windows station must do a log on to get # to the interactive desktop. Running process user account must have # the 'Replace a process level token' permission. This is necessary as # the logon (which happens with CreateProcessWithLogon) must have an # interactive windows station to attach to, which is created with the # LogonUser cann with the LOGON32_LOGON_INTERACTIVE flag. if winsta_name =~ /^Service-0x0-.*$/i token = FFI::MemoryPointer.new(:ulong) bool = LogonUserW( logon, # User domain, # Domain passwd, # Password LOGON32_LOGON_INTERACTIVE, # Logon Type LOGON32_PROVIDER_DEFAULT, # Logon Provider token # User token handle ) unless bool raise SystemCallError.new("LogonUserW", FFI.errno) end token = token.read_ulong bool = CreateProcessAsUserW( token, # User token handle app, # App name cmd, # Command line process_security, # Process attributes thread_security, # Thread attributes inherit, # Inherit handles hash['creation_flags'], # Creation Flags env, # Environment cwd, # Working directory startinfo, # Startup Info procinfo # Process Info ) unless bool raise SystemCallError.new("CreateProcessAsUserW (You must hold the 'Replace a process level token' permission)", FFI.errno) end else bool = CreateProcessWithLogonW( logon, # User domain, # Domain passwd, # Password LOGON_WITH_PROFILE, # Logon flags app, # App name cmd, # Command line hash['creation_flags'], # Creation flags env, # Environment cwd, # Working directory startinfo, # Startup Info procinfo # Process Info ) end unless bool raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno) end else bool = CreateProcessW( app, # App name cmd, # Command line process_security, # Process attributes thread_security, # Thread attributes inherit, # Inherit handles? hash['creation_flags'], # Creation flags env, # Environment cwd, # Working directory startinfo, # Startup Info procinfo # Process Info ) unless bool raise SystemCallError.new("CreateProcessW", FFI.errno) end end # Automatically close the process and thread handles in the # PROCESS_INFORMATION struct unless explicitly told not to. if hash['close_handles'] CloseHandle(procinfo[:hProcess]) CloseHandle(procinfo[:hThread]) CloseHandle(token) end ProcessInfo.new( procinfo[:hProcess], procinfo[:hThread], procinfo[:dwProcessId], procinfo[:dwThreadId] ) end module_function :create end mixlib-shellout-1.3.0/lib/mixlib/shellout/version.rb0000644000004100000410000000007712261203217022567 0ustar www-datawww-datamodule Mixlib class ShellOut VERSION = "1.3.0" end end mixlib-shellout-1.3.0/lib/mixlib/shellout/windows.rb0000644000004100000410000002255712261203217022603 0ustar www-datawww-data#-- # Author:: Daniel DeLeo () # Author:: John Keiser () # Author:: Ho-Sheng Hsiao () # Copyright:: Copyright (c) 2011, 2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # 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. # See the License for the specific language governing permissions and # limitations under the License. # require 'win32/process' require 'windows/handle' require 'windows/process' require 'windows/synchronize' require 'mixlib/shellout/windows/core_ext' module Mixlib class ShellOut module Windows include ::Windows::Handle include ::Windows::Process include ::Windows::Synchronize TIME_SLICE = 0.05 # Option validation that is windows specific def validate_options(opts) if opts[:user] unless opts[:password] raise InvalidCommandOption, "You must supply both a username and password when supplying a user in windows" end end end #-- # Missing lots of features from the UNIX version, such as # uid, etc. def run_command # # Create pipes to capture stdout and stderr, # stdout_read, stdout_write = IO.pipe stderr_read, stderr_write = IO.pipe stdin_read, stdin_write = IO.pipe open_streams = [ stdout_read, stderr_read ] begin # # Set cwd, environment, appname, etc. # app_name, command_line = command_to_run(self.command) create_process_args = { :app_name => app_name, :command_line => command_line, :startup_info => { :stdout => stdout_write, :stderr => stderr_write, :stdin => stdin_read }, :environment => inherit_environment.map { |k,v| "#{k}=#{v}" }, :close_handles => false } create_process_args[:cwd] = cwd if cwd # default to local account database if domain is not specified create_process_args[:domain] = domain.nil? ? "." : domain create_process_args[:with_logon] = with_logon if with_logon create_process_args[:password] = password if password # # Start the process # process = Process.create(create_process_args) begin # Start pushing data into input stdin_write << input if input # Close pipe to kick things off stdin_write.close # # Wait for the process to finish, consuming output as we go # start_wait = Time.now while true wait_status = WaitForSingleObject(process.process_handle, 0) case wait_status when WAIT_OBJECT_0 # Get process exit code exit_code = [0].pack('l') unless GetExitCodeProcess(process.process_handle, exit_code) raise get_last_error end @status = ThingThatLooksSortOfLikeAProcessStatus.new @status.exitstatus = exit_code.unpack('l').first return self when WAIT_TIMEOUT # Kill the process if (Time.now - start_wait) > timeout raise Mixlib::ShellOut::CommandTimeout, "command timed out:\n#{format_for_exception}" end consume_output(open_streams, stdout_read, stderr_read) else raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}" end end ensure CloseHandle(process.thread_handle) CloseHandle(process.process_handle) end ensure # # Consume all remaining data from the pipes until they are closed # stdout_write.close stderr_write.close while consume_output(open_streams, stdout_read, stderr_read) end end end private class ThingThatLooksSortOfLikeAProcessStatus attr_accessor :exitstatus def success? exitstatus == 0 end end def consume_output(open_streams, stdout_read, stderr_read) return false if open_streams.length == 0 ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME) return true if ! ready if ready.first.include?(stdout_read) begin next_chunk = stdout_read.readpartial(READ_SIZE) @stdout << next_chunk @live_stream << next_chunk if @live_stream rescue EOFError stdout_read.close open_streams.delete(stdout_read) end end if ready.first.include?(stderr_read) begin @stderr << stderr_read.readpartial(READ_SIZE) rescue EOFError stderr_read.close open_streams.delete(stderr_read) end end return true end IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i def command_to_run(command) return _run_under_cmd(command) if Utils.should_run_under_cmd?(command) candidate = candidate_executable_for_command(command) # Don't do searching for empty commands. Let it fail when it runs. return [ nil, command ] if candidate.length == 0 # Check if the exe exists directly. Otherwise, search PATH. exe = Utils.find_executable(candidate) exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/ # Batch files MUST use cmd; and if we couldn't find the command we're looking for, # we assume it must be a cmd builtin. if exe.nil? || exe =~ IS_BATCH_FILE _run_under_cmd(command) else _run_directly(command, exe) end end # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes. # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859 # http://ss64.com/nt/syntax-esc.html def _run_under_cmd(command) [ ENV['COMSPEC'], "cmd /c \"#{command}\"" ] end def _run_directly(command, exe) [ exe, command ] end def unquoted_executable_path(command) command[0,command.index(/\s/) || command.length] end def candidate_executable_for_command(command) if command =~ /^\s*"(.*?)"/ # If we have quotes, do an exact match $1 else # Otherwise check everything up to the first space unquoted_executable_path(command).strip end end def inherit_environment result = {} ENV.each_pair do |k,v| result[k] = v end environment.each_pair do |k,v| if v == nil result.delete(k) else result[k] = v end end result end module Utils # api: semi-private # If there are special characters parsable by cmd.exe (such as file redirection), then # this method should return true. # # This parser is based on # https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437 def self.should_run_under_cmd?(command) return true if command =~ /^@/ quote = nil env = false env_first_char = false command.dup.each_char do |c| case c when "'", '"' if (!quote) quote = c elsif quote == c quote = nil end next when '>', '<', '|', '&', "\n" return true unless quote when '%' return true if env env = env_first_char = true next else next unless env if env_first_char env_first_char = false env = false and next if c !~ /[A-Za-z_]/ end env = false if c !~ /[A-Za-z1-9_]/ end end return false end def self.pathext @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : [''] end # which() mimicks the Unix which command # FIXME: it is not working def self.which(cmd) ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| exe = find_executable("#{path}/#{cmd}") return exe if exe end return nil end # Windows has a different notion of what "executable" means # The OS will search through valid the extensions and look # for a binary there. def self.find_executable(path) return path if File.executable? path pathext.each do |ext| exe = "#{path}#{ext}" return exe if File.executable? exe end return nil end end end # class end end mixlib-shellout-1.3.0/lib/mixlib/shellout/exceptions.rb0000644000004100000410000000027212261203217023260 0ustar www-datawww-datamodule Mixlib class ShellOut class ShellCommandFailed < RuntimeError; end class CommandTimeout < RuntimeError; end class InvalidCommandOption < RuntimeError; end end end mixlib-shellout-1.3.0/lib/mixlib/shellout/unix.rb0000644000004100000410000002500412261203217022062 0ustar www-datawww-data#-- # Author:: Daniel DeLeo () # Copyright:: Copyright (c) 2010, 2011 Opscode, Inc. # License:: Apache License, Version 2.0 # # 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. # See the License for the specific language governing permissions and # limitations under the License. # module Mixlib class ShellOut module Unix # Option validation that is unix specific def validate_options(opts) # No options to validate, raise exceptions here if needed 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 # returns +self+; +stdout+, +stderr+, +status+, and +exitstatus+ will be # populated with results of the command # === Raises # * Errno::EACCES when you are not privileged to execute the command # * Errno::ENOENT when the command is not available on the system (or not # in the current $PATH) # * Chef::Exceptions::CommandTimeout when the command does not complete # within +timeout+ seconds (default: 600s) def run_command @child_pid = fork_subprocess @reaped = false configure_parent_process_file_descriptors # 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* # make it work. So I give you this epic hack: GC.disable # CHEF-3390: Marshall.load on Ruby < 1.8.7p369 also has a GC bug related # to Marshall.load, so try disabling GC first. propagate_pre_exec_failure @result = nil @execution_time = 0 write_to_child_stdin until @status ready_buffers = attempt_buffer_read unless ready_buffers @execution_time += READ_WAIT_TIME if @execution_time >= timeout && !@result # kill the bad proccess reap_errant_child # read anything it wrote when we killed it attempt_buffer_read # raise raise CommandTimeout, "Command timed out after #{@execution_time.to_i}s:\n#{format_for_exception}" end end attempt_reap end self rescue Errno::ENOENT # When ENOENT happens, we can be reasonably sure that the child process # is going to exit quickly, so we use the blocking variant of waitpid2 reap raise ensure reap_errant_child if should_reap? # make one more pass to get the last of the output after the # child process dies attempt_buffer_read # no matter what happens, turn the GC back on, and hope whatever busted # version of ruby we're on doesn't allocate some objects during the next # GC run. GC.enable close_all_pipes end private def set_user if user Process.euid = uid Process.uid = uid 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 def initialize_ipc @stdin_pipe, @stdout_pipe, @stderr_pipe, @process_status_pipe = IO.pipe, IO.pipe, IO.pipe, IO.pipe @process_status_pipe.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) end def child_stdin @stdin_pipe[1] 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_stdin.close unless child_stdin.closed? 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. # # If there is no input, 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_pipe.last.close STDIN.reopen stdin_pipe.first stdin_pipe.first.close unless input stdout_pipe.first.close STDOUT.reopen stdout_pipe.last stdout_pipe.last.close stderr_pipe.first.close STDERR.reopen stderr_pipe.last stderr_pipe.last.close STDOUT.sync = STDERR.sync = true STDIN.sync = true if input end # When a new process is started with chef, it shares the file # descriptors of the parent. We clean the file descriptors # coming from the parent to prevent unintended locking if parent # is killed. # NOTE: After some discussions we've decided to iterate on file # descriptors upto 256. We believe this is a reasonable upper # limit in a chef environment. If we have issues in the future this # number could be made to be configurable or updated based on # the ulimit based on platform. def clean_parent_file_descriptors # Don't clean $stdin, $stdout, $stderr, process_status_pipe. 3.upto(256) do |n| # We are checking the fd for error pipe before attempting to # create a file because error pipe will auto close when we # try to create a file since it's set to CLOEXEC. if n != @process_status_pipe.last.to_i begin fd = File.for_fd(n) fd.close if fd rescue end end end end def configure_parent_process_file_descriptors # Close the sides of the pipes we don't care about stdin_pipe.first.close stdin_pipe.last.close unless input stdout_pipe.last.close stderr_pipe.last.close process_status_pipe.last.close # Get output as it happens rather than buffered child_stdin.sync = true if input child_stdout.sync = true child_stderr.sync = true 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 @open_pipes ||= [child_stdout, child_stderr] end # Keep this unbuffered for now def write_to_child_stdin return unless input child_stdin << input child_stdin.close # Kick things off end def attempt_buffer_read ready = IO.select(open_pipes, nil, nil, READ_WAIT_TIME) if ready && ready.first.include?(child_stdout) read_stdout_to_buffer end if ready && ready.first.include?(child_stderr) read_stderr_to_buffer end ready end def read_stdout_to_buffer while chunk = child_stdout.read_nonblock(READ_SIZE) @stdout << chunk @live_stream << chunk if @live_stream end rescue Errno::EAGAIN rescue EOFError open_pipes.delete(child_stdout) end def read_stderr_to_buffer while chunk = child_stderr.read_nonblock(READ_SIZE) @stderr << chunk end rescue Errno::EAGAIN rescue EOFError open_pipes.delete(child_stderr) end def fork_subprocess initialize_ipc fork do configure_subprocess_file_descriptors clean_parent_file_descriptors set_group set_user set_environment set_umask set_cwd 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) process_status_pipe.last.flush end process_status_pipe.last.close unless (process_status_pipe.last.closed?) 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. def propagate_pre_exec_failure begin e = Marshal.load child_process_status raise(Exception === e ? e : "unknown failure: #{e.inspect}") rescue EOFError # If we get an EOF error, then the exec was successful true ensure child_process_status.close end end def reap_errant_child return if attempt_reap @terminate_reason = "Command execeded allowed execution time, killed by TERM signal." logger.error("Command execeded allowed execution time, sending TERM") if logger Process.kill(:TERM, @child_pid) sleep 3 return if attempt_reap @terminate_reason = "Command execeded allowed execution time, did not respond to TERM. Killed by KILL signal." logger.error("Command did not exit from TERM, sending KILL") if logger Process.kill(:KILL, @child_pid) reap # Should not hit this but it's possible if something is calling waitall # in a separate thread. rescue Errno::ESRCH nil end def should_reap? # if we fail to fork, no child pid so nothing to reap @child_pid && !@reaped end def reap results = Process.waitpid2(@child_pid) @reaped = true @status = results.last end def attempt_reap if results = Process.waitpid2(@child_pid, Process::WNOHANG) @reaped = true @status = results.last else nil end end end end end mixlib-shellout-1.3.0/metadata.yml0000644000004100000410000000306712261203217017171 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: mixlib-shellout version: !ruby/object:Gem::Version version: 1.3.0 prerelease: platform: ruby authors: - Opscode autorequire: bindir: bin cert_chain: [] date: 2013-12-03 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: '2.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: '2.0' description: Run external commands on Unix or Windows email: info@opscode.com executables: [] extensions: [] extra_rdoc_files: - README.md - LICENSE files: - LICENSE - README.md - lib/mixlib/shellout/exceptions.rb - lib/mixlib/shellout/unix.rb - lib/mixlib/shellout/version.rb - lib/mixlib/shellout/windows/core_ext.rb - lib/mixlib/shellout/windows.rb - lib/mixlib/shellout.rb homepage: http://wiki.opscode.com/ licenses: [] post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 1.8.23 signing_key: specification_version: 3 summary: Run external commands on Unix or Windows test_files: [] mixlib-shellout-1.3.0/LICENSE0000644000004100000410000002514212261203217015671 0ustar www-datawww-data Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. See the License for the specific language governing permissions and limitations under the License. mixlib-shellout-1.3.0/README.md0000644000004100000410000000317512261203217016145 0ustar www-datawww-data# Mixlib::ShellOut 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. ## Example Invoke find(1) to search for .rb files: find = Mixlib::ShellOut.new("find . -name '*.rb'") find.run_command If all went well, the results are on `stdout` puts find.stdout `find(1)` prints diagnostic info to STDERR: puts "error messages" + find.stderr Raise an exception if it didn't exit with 0 find.error! Run a command as the `www` user with no extra ENV settings from `/tmp` cmd = Mixlib::ShellOut.new("apachectl", "start", :user => 'www', :env => nil, :cwd => '/tmp') cmd.run_command # etc. ## STDIN Example Invoke crontab to edit user cron: # :input only supports simple strings crontab_lines = [ "* * * * * /bin/true", "* * * * * touch /tmp/here" ] crontab = Mixlib::ShellOut.new("crontab -l -u #{@new_resource.user}", :input => crontab_lines.join("\n")) crontab.run_command ## Windows Impersonation Example Invoke crontab to edit user cron: whomai = Mixlib::ShellOut.new("whoami.exe", :user => "username", :domain => "DOMAIN", :password => "password") whoami.run_command ## Platform Support Mixlib::ShellOut does a standard fork/exec on Unix, and uses the Win32 API on Windows. There is not currently support for JRuby. ## License Apache 2 Licensed. See LICENSE for full details. ## See Also * `Process.spawn` in Ruby 1.9 * [https://github.com/rtomayko/posix-spawn](posix-spawn)