diff options
author | Ho-Sheng Hsiao <hosheng.hsiao@gmail.com> | 2012-05-11 14:03:52 -0700 |
---|---|---|
committer | Ho-Sheng Hsiao <hosheng.hsiao@gmail.com> | 2012-05-11 14:03:52 -0700 |
commit | a8960e798a4229eea315a455b503cd77fc0b55a3 (patch) | |
tree | ebcfdef48739abded29ded7931d5cae4281770e4 | |
parent | 3a72a18a9151b160cea1e47f226fc45ba295ed8e (diff) | |
parent | e2ef96f211d8e38e844f90537abb9cd27921e187 (diff) | |
download | chef-a8960e798a4229eea315a455b503cd77fc0b55a3.tar.gz |
Merge pull request #4 from hosh/chef-2994-stdin-support-r1
[CHEF-2994] Mixlib::ShellOut should support STDIN
-rw-r--r-- | README.md | 8 | ||||
-rw-r--r-- | lib/mixlib/shellout.rb | 8 | ||||
-rw-r--r-- | lib/mixlib/shellout/exceptions.rb | 1 | ||||
-rw-r--r-- | lib/mixlib/shellout/unix.rb | 35 | ||||
-rw-r--r-- | lib/mixlib/shellout/windows.rb | 508 | ||||
-rw-r--r-- | lib/mixlib/shellout/windows/core_ext.rb | 385 | ||||
-rw-r--r-- | spec/mixlib/shellout/windows_spec.rb | 283 | ||||
-rw-r--r-- | spec/mixlib/shellout_spec.rb (renamed from spec/mixlib/shellout/shellout_spec.rb) | 242 | ||||
-rw-r--r-- | spec/spec_helper.rb | 3 | ||||
-rw-r--r-- | spec/support/platform_helpers.rb | 8 |
10 files changed, 1047 insertions, 434 deletions
@@ -28,6 +28,14 @@ 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 + ## 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. diff --git a/lib/mixlib/shellout.rb b/lib/mixlib/shellout.rb index 990637294e..8e8b7ea53a 100644 --- a/lib/mixlib/shellout.rb +++ b/lib/mixlib/shellout.rb @@ -55,6 +55,11 @@ module Mixlib # 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 @@ -140,6 +145,7 @@ module Mixlib def initialize(*command_args) @stdout, @stderr = '', '' @live_stream = nil + @input = nil @log_level = :debug @log_tag = nil @environment = DEFAULT_ENVIRONMENT @@ -267,6 +273,8 @@ module Mixlib 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' diff --git a/lib/mixlib/shellout/exceptions.rb b/lib/mixlib/shellout/exceptions.rb index 417def3aac..16b1946f27 100644 --- a/lib/mixlib/shellout/exceptions.rb +++ b/lib/mixlib/shellout/exceptions.rb @@ -5,4 +5,3 @@ module Mixlib class InvalidCommandOption < RuntimeError; end end end - diff --git a/lib/mixlib/shellout/unix.rb b/lib/mixlib/shellout/unix.rb index fe307b5e61..aa62272b37 100644 --- a/lib/mixlib/shellout/unix.rb +++ b/lib/mixlib/shellout/unix.rb @@ -40,6 +40,8 @@ module Mixlib @result = nil @execution_time = 0 + write_to_child_stdin + # 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* @@ -114,10 +116,14 @@ module Mixlib end def initialize_ipc - @stdout_pipe, @stderr_pipe, @process_status_pipe = IO.pipe, IO.pipe, IO.pipe + @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 @@ -131,23 +137,25 @@ module Mixlib 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. Close STDIN so when we - # exec, the new program will know it's never getting input ever. + # 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_reader, stdin_writer = IO.pipe - stdin_writer.close - STDIN.reopen stdin_reader - stdin_reader.close + stdin_pipe.last.close + STDIN.reopen stdin_pipe.first + stdin_pipe.first.close unless input stdout_pipe.first.close STDOUT.reopen stdout_pipe.last @@ -158,14 +166,18 @@ module Mixlib stderr_pipe.last.close STDOUT.sync = STDERR.sync = true + STDIN.sync = true if input 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 @@ -178,6 +190,13 @@ module Mixlib @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 read_stdout_to_buffer while chunk = child_stdout.read_nonblock(READ_SIZE) @stdout << chunk diff --git a/lib/mixlib/shellout/windows.rb b/lib/mixlib/shellout/windows.rb index 89bdc22bbc..7bebb3bfb3 100644 --- a/lib/mixlib/shellout/windows.rb +++ b/lib/mixlib/shellout/windows.rb @@ -1,7 +1,8 @@ #-- # Author:: Daniel DeLeo (<dan@opscode.com>) # Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2011 Opscode, Inc. +# Author:: Ho-Sheng Hsiao (<hosh@opscode.com>) +# Copyright:: Copyright (c) 2011, 2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +23,8 @@ require 'windows/handle' require 'windows/process' require 'windows/synchronize' +require 'mixlib/shellout/windows/core_ext' + module Mixlib class ShellOut module Windows @@ -50,7 +53,7 @@ module Mixlib # # Set cwd, environment, appname, etc. # - app_name, command_line = command_to_run + app_name, command_line = command_to_run(self.command) create_process_args = { :app_name => app_name, :command_line => command_line, @@ -69,6 +72,11 @@ module Mixlib # 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 @@ -90,7 +98,7 @@ module Mixlib when WAIT_TIMEOUT # Kill the process if (Time.now - start_wait) > timeout - raise Mixlib::ShellOut::Exceptions::CommandTimeout, "command timed out:\n#{format_for_exception}" + raise Mixlib::ShellOut::CommandTimeout, "command timed out:\n#{format_for_exception}" end consume_output(open_streams, stdout_read, stderr_read) @@ -154,33 +162,52 @@ module Mixlib return true end - IS_BATCH_FILE = /\.bat|\.cmd$/i + IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i - def command_to_run - if command =~ /^\s*"(.*)"/ - # If we have quotes, do an exact match - candidate = $1 - else - # Otherwise check everything up to the first space - candidate = command[0,command.index(/\s/) || command.length].strip - end + 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. - if candidate.length == 0 - return [ nil, command ] - end + return [ nil, command ] if candidate.length == 0 # Check if the exe exists directly. Otherwise, search PATH. - exe = find_exe_at_location(candidate) - if exe.nil? && exe !~ /[\\\/]/ - exe = which(command[0,command.index(/\s/) || command.length]) - end + 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 - # 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. - [ ENV['COMSPEC'], "cmd /c #{command}" ] + _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 - [ exe, command ] + # Otherwise check everything up to the first space + unquoted_executable_path(command).strip end end @@ -200,389 +227,74 @@ module Mixlib result end - def pathext - @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : [''] - end - - def which(cmd) - ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| - exe = find_exe_at_location("#{path}/${cmd}") - return exe if exe - end - return nil - end - - def find_exe_at_location(path) - return path if File.executable? path - pathext.each do |ext| - exe = "#{path}#{ext}" - return exe if File.executable? exe + 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 - return nil - end - end # class - end -end - -# -# Override module Windows::Process.CreateProcess to fix bug when -# using both app_name and command_line -# -module Windows - module Process - API.new('CreateProcess', 'SPPPLLLPPP', 'B') - end -end - -# -# Override Win32::Process.create to take a proper environment hash -# so that variables can contain semicolons -# (submitted patch to owner) -# -module Process - def create(args) - unless args.kind_of?(Hash) - raise TypeError, 'Expecting hash-style keyword arguments' - 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 = %/ - 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 - - # The environment string should be passed as an array of A=B paths, or - # as a string of ';' separated paths. - if hash['environment'] - env = hash['environment'] - if !env.respond_to?(:join) - # Backwards compat for ; separated paths - env = hash['environment'].split(File::PATH_SEPARATOR) - end - # The argument format is a series of null-terminated strings, with an additional null terminator. - env = env.map { |e| e + "\0" }.join("") + "\0" - if hash['with_logon'] - env = env.multi_to_wide(e) - end - env = [env].pack('p*').unpack('L').first - else - env = nil - end - - startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL') - procinfo = [0,0,0,0].pack('LLLL') - - # Process SECURITY_ATTRIBUTE structure - process_security = 0 - if hash['process_inherit'] - process_security = [0,0,0].pack('LLL') - process_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE) - process_security[8,4] = [1].pack('L') # TRUE - end - - # Thread SECURITY_ATTRIBUTE structure - thread_security = 0 - if hash['thread_inherit'] - thread_security = [0,0,0].pack('LLL') - thread_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE) - thread_security[8,4] = [1].pack('L') # TRUE - end - - # Automatically handle stdin, stdout and stderr as either IO objects - # or file descriptors. This won't work for StringIO, however. - ['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]) + def self.pathext + @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : [''] end - if handle == INVALID_HANDLE_VALUE - raise Error, get_last_error + # 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 - # 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 - ) + # 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 - raise Error, get_last_error unless bool - - si_hash[io] = handle - si_hash['startf_flags'] ||= 0 - si_hash['startf_flags'] |= STARTF_USESTDHANDLES - hash['inherit'] = true + pathext.each do |ext| + exe = "#{path}#{ext}" + return exe if File.executable? exe + end + return nil + end end - } - - # The bytes not covered here are reserved (null) - unless si_hash.empty? - startinfo[0,4] = [startinfo.size].pack('L') - startinfo[8,4] = [si_hash['desktop']].pack('p*') if si_hash['desktop'] - startinfo[12,4] = [si_hash['title']].pack('p*') if si_hash['title'] - startinfo[16,4] = [si_hash['x']].pack('L') if si_hash['x'] - startinfo[20,4] = [si_hash['y']].pack('L') if si_hash['y'] - startinfo[24,4] = [si_hash['x_size']].pack('L') if si_hash['x_size'] - startinfo[28,4] = [si_hash['y_size']].pack('L') if si_hash['y_size'] - startinfo[32,4] = [si_hash['x_count_chars']].pack('L') if si_hash['x_count_chars'] - startinfo[36,4] = [si_hash['y_count_chars']].pack('L') if si_hash['y_count_chars'] - startinfo[40,4] = [si_hash['fill_attribute']].pack('L') if si_hash['fill_attribute'] - startinfo[44,4] = [si_hash['startf_flags']].pack('L') if si_hash['startf_flags'] - startinfo[48,2] = [si_hash['sw_flags']].pack('S') if si_hash['sw_flags'] - startinfo[56,4] = [si_hash['stdin']].pack('L') if si_hash['stdin'] - startinfo[60,4] = [si_hash['stdout']].pack('L') if si_hash['stdout'] - startinfo[64,4] = [si_hash['stderr']].pack('L') if si_hash['stderr'] - end - - if hash['with_logon'] - logon = multi_to_wide(hash['with_logon']) - domain = multi_to_wide(hash['domain']) - app = hash['app_name'].nil? ? nil : multi_to_wide(hash['app_name']) - cmd = hash['command_line'].nil? ? nil : multi_to_wide(hash['command_line']) - cwd = multi_to_wide(hash['cwd']) - passwd = multi_to_wide(hash['password']) - - hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT - - process_ran = 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 - ) - else - process_ran = CreateProcess( - hash['app_name'], # App name - hash['command_line'], # Command line - process_security, # Process attributes - thread_security, # Thread attributes - hash['inherit'], # Inherit handles? - hash['creation_flags'], # Creation flags - env, # Environment - hash['cwd'], # Working directory - startinfo, # Startup Info - procinfo # Process Info - ) - end - - # TODO: Close stdin, stdout and stderr handles in the si_hash unless - # they're pointing to one of the standard handles already. [Maybe] - if !process_ran - raise_last_error("CreateProcess()") - end - - # Automatically close the process and thread handles in the - # PROCESS_INFORMATION struct unless explicitly told not to. - if hash['close_handles'] - CloseHandle(procinfo[0,4].unpack('L').first) - CloseHandle(procinfo[4,4].unpack('L').first) - end - - ProcessInfo.new( - procinfo[0,4].unpack('L').first, # hProcess - procinfo[4,4].unpack('L').first, # hThread - procinfo[8,4].unpack('L').first, # hProcessId - procinfo[12,4].unpack('L').first # hThreadId - ) - end - - def self.raise_last_error(operation) - error_string = "#{operation} failed: #{get_last_error}" - last_error_code = GetLastError() - if ERROR_CODE_MAP.has_key?(last_error_code) - raise ERROR_CODE_MAP[last_error_code], error_string - else - raise Error, error_string - end + end # class end - - # List from ruby/win32/win32.c - ERROR_CODE_MAP = { - ERROR_INVALID_FUNCTION => Errno::EINVAL, - ERROR_FILE_NOT_FOUND => Errno::ENOENT, - ERROR_PATH_NOT_FOUND => Errno::ENOENT, - ERROR_TOO_MANY_OPEN_FILES => Errno::EMFILE, - ERROR_ACCESS_DENIED => Errno::EACCES, - ERROR_INVALID_HANDLE => Errno::EBADF, - ERROR_ARENA_TRASHED => Errno::ENOMEM, - ERROR_NOT_ENOUGH_MEMORY => Errno::ENOMEM, - ERROR_INVALID_BLOCK => Errno::ENOMEM, - ERROR_BAD_ENVIRONMENT => Errno::E2BIG, - ERROR_BAD_FORMAT => Errno::ENOEXEC, - ERROR_INVALID_ACCESS => Errno::EINVAL, - ERROR_INVALID_DATA => Errno::EINVAL, - ERROR_INVALID_DRIVE => Errno::ENOENT, - ERROR_CURRENT_DIRECTORY => Errno::EACCES, - ERROR_NOT_SAME_DEVICE => Errno::EXDEV, - ERROR_NO_MORE_FILES => Errno::ENOENT, - ERROR_WRITE_PROTECT => Errno::EROFS, - ERROR_BAD_UNIT => Errno::ENODEV, - ERROR_NOT_READY => Errno::ENXIO, - ERROR_BAD_COMMAND => Errno::EACCES, - ERROR_CRC => Errno::EACCES, - ERROR_BAD_LENGTH => Errno::EACCES, - ERROR_SEEK => Errno::EIO, - ERROR_NOT_DOS_DISK => Errno::EACCES, - ERROR_SECTOR_NOT_FOUND => Errno::EACCES, - ERROR_OUT_OF_PAPER => Errno::EACCES, - ERROR_WRITE_FAULT => Errno::EIO, - ERROR_READ_FAULT => Errno::EIO, - ERROR_GEN_FAILURE => Errno::EACCES, - ERROR_LOCK_VIOLATION => Errno::EACCES, - ERROR_SHARING_VIOLATION => Errno::EACCES, - ERROR_WRONG_DISK => Errno::EACCES, - ERROR_SHARING_BUFFER_EXCEEDED => Errno::EACCES, -# ERROR_BAD_NETPATH => Errno::ENOENT, -# ERROR_NETWORK_ACCESS_DENIED => Errno::EACCES, -# ERROR_BAD_NET_NAME => Errno::ENOENT, - ERROR_FILE_EXISTS => Errno::EEXIST, - ERROR_CANNOT_MAKE => Errno::EACCES, - ERROR_FAIL_I24 => Errno::EACCES, - ERROR_INVALID_PARAMETER => Errno::EINVAL, - ERROR_NO_PROC_SLOTS => Errno::EAGAIN, - ERROR_DRIVE_LOCKED => Errno::EACCES, - ERROR_BROKEN_PIPE => Errno::EPIPE, - ERROR_DISK_FULL => Errno::ENOSPC, - ERROR_INVALID_TARGET_HANDLE => Errno::EBADF, - ERROR_INVALID_HANDLE => Errno::EINVAL, - ERROR_WAIT_NO_CHILDREN => Errno::ECHILD, - ERROR_CHILD_NOT_COMPLETE => Errno::ECHILD, - ERROR_DIRECT_ACCESS_HANDLE => Errno::EBADF, - ERROR_NEGATIVE_SEEK => Errno::EINVAL, - ERROR_SEEK_ON_DEVICE => Errno::EACCES, - ERROR_DIR_NOT_EMPTY => Errno::ENOTEMPTY, -# ERROR_DIRECTORY => Errno::ENOTDIR, - ERROR_NOT_LOCKED => Errno::EACCES, - ERROR_BAD_PATHNAME => Errno::ENOENT, - ERROR_MAX_THRDS_REACHED => Errno::EAGAIN, -# ERROR_LOCK_FAILED => Errno::EACCES, - ERROR_ALREADY_EXISTS => Errno::EEXIST, - ERROR_INVALID_STARTING_CODESEG => Errno::ENOEXEC, - ERROR_INVALID_STACKSEG => Errno::ENOEXEC, - ERROR_INVALID_MODULETYPE => Errno::ENOEXEC, - ERROR_INVALID_EXE_SIGNATURE => Errno::ENOEXEC, - ERROR_EXE_MARKED_INVALID => Errno::ENOEXEC, - ERROR_BAD_EXE_FORMAT => Errno::ENOEXEC, - ERROR_ITERATED_DATA_EXCEEDS_64k => Errno::ENOEXEC, - ERROR_INVALID_MINALLOCSIZE => Errno::ENOEXEC, - ERROR_DYNLINK_FROM_INVALID_RING => Errno::ENOEXEC, - ERROR_IOPL_NOT_ENABLED => Errno::ENOEXEC, - ERROR_INVALID_SEGDPL => Errno::ENOEXEC, - ERROR_AUTODATASEG_EXCEEDS_64k => Errno::ENOEXEC, - ERROR_RING2SEG_MUST_BE_MOVABLE => Errno::ENOEXEC, - ERROR_RELOC_CHAIN_XEEDS_SEGLIM => Errno::ENOEXEC, - ERROR_INFLOOP_IN_RELOC_CHAIN => Errno::ENOEXEC, - ERROR_FILENAME_EXCED_RANGE => Errno::ENOENT, - ERROR_NESTING_NOT_ALLOWED => Errno::EAGAIN, -# ERROR_PIPE_LOCAL => Errno::EPIPE, - ERROR_BAD_PIPE => Errno::EPIPE, - ERROR_PIPE_BUSY => Errno::EAGAIN, - ERROR_NO_DATA => Errno::EPIPE, - ERROR_PIPE_NOT_CONNECTED => Errno::EPIPE, - ERROR_OPERATION_ABORTED => Errno::EINTR, -# ERROR_NOT_ENOUGH_QUOTA => Errno::ENOMEM, - ERROR_MOD_NOT_FOUND => Errno::ENOENT, - WSAEINTR => Errno::EINTR, - WSAEBADF => Errno::EBADF, -# WSAEACCES => Errno::EACCES, - WSAEFAULT => Errno::EFAULT, - WSAEINVAL => Errno::EINVAL, - WSAEMFILE => Errno::EMFILE, - WSAEWOULDBLOCK => Errno::EWOULDBLOCK, - WSAEINPROGRESS => Errno::EINPROGRESS, - WSAEALREADY => Errno::EALREADY, - WSAENOTSOCK => Errno::ENOTSOCK, - WSAEDESTADDRREQ => Errno::EDESTADDRREQ, - WSAEMSGSIZE => Errno::EMSGSIZE, - WSAEPROTOTYPE => Errno::EPROTOTYPE, - WSAENOPROTOOPT => Errno::ENOPROTOOPT, - WSAEPROTONOSUPPORT => Errno::EPROTONOSUPPORT, - WSAESOCKTNOSUPPORT => Errno::ESOCKTNOSUPPORT, - WSAEOPNOTSUPP => Errno::EOPNOTSUPP, - WSAEPFNOSUPPORT => Errno::EPFNOSUPPORT, - WSAEAFNOSUPPORT => Errno::EAFNOSUPPORT, - WSAEADDRINUSE => Errno::EADDRINUSE, - WSAEADDRNOTAVAIL => Errno::EADDRNOTAVAIL, - WSAENETDOWN => Errno::ENETDOWN, - WSAENETUNREACH => Errno::ENETUNREACH, - WSAENETRESET => Errno::ENETRESET, - WSAECONNABORTED => Errno::ECONNABORTED, - WSAECONNRESET => Errno::ECONNRESET, - WSAENOBUFS => Errno::ENOBUFS, - WSAEISCONN => Errno::EISCONN, - WSAENOTCONN => Errno::ENOTCONN, - WSAESHUTDOWN => Errno::ESHUTDOWN, - WSAETOOMANYREFS => Errno::ETOOMANYREFS, -# WSAETIMEDOUT => Errno::ETIMEDOUT, - WSAECONNREFUSED => Errno::ECONNREFUSED, - WSAELOOP => Errno::ELOOP, - WSAENAMETOOLONG => Errno::ENAMETOOLONG, - WSAEHOSTDOWN => Errno::EHOSTDOWN, - WSAEHOSTUNREACH => Errno::EHOSTUNREACH, -# WSAEPROCLIM => Errno::EPROCLIM, -# WSAENOTEMPTY => Errno::ENOTEMPTY, - WSAEUSERS => Errno::EUSERS, - WSAEDQUOT => Errno::EDQUOT, - WSAESTALE => Errno::ESTALE, - WSAEREMOTE => Errno::EREMOTE - } - - module_function :create end diff --git a/lib/mixlib/shellout/windows/core_ext.rb b/lib/mixlib/shellout/windows/core_ext.rb new file mode 100644 index 0000000000..be25127d00 --- /dev/null +++ b/lib/mixlib/shellout/windows/core_ext.rb @@ -0,0 +1,385 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: John Keiser (<jkeiser@opscode.com>) +# 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' + +# Override module Windows::Process.CreateProcess to fix bug when +# using both app_name and command_line +# +module Windows + module Process + API.new('CreateProcess', 'SPPPLLLPPP', 'B') + end +end + +# +# Override Win32::Process.create to take a proper environment hash +# so that variables can contain semicolons +# (submitted patch to owner) +# +module Process + def create(args) + unless args.kind_of?(Hash) + raise TypeError, 'Expecting hash-style keyword arguments' + 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 = %/ + 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 + + # The environment string should be passed as an array of A=B paths, or + # as a string of ';' separated paths. + if hash['environment'] + env = hash['environment'] + if !env.respond_to?(:join) + # Backwards compat for ; separated paths + env = hash['environment'].split(File::PATH_SEPARATOR) + end + # The argument format is a series of null-terminated strings, with an additional null terminator. + env = env.map { |e| e + "\0" }.join("") + "\0" + if hash['with_logon'] + env = env.multi_to_wide(e) + end + env = [env].pack('p*').unpack('L').first + else + env = nil + end + + startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL') + procinfo = [0,0,0,0].pack('LLLL') + + # Process SECURITY_ATTRIBUTE structure + process_security = 0 + if hash['process_inherit'] + process_security = [0,0,0].pack('LLL') + process_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE) + process_security[8,4] = [1].pack('L') # TRUE + end + + # Thread SECURITY_ATTRIBUTE structure + thread_security = 0 + if hash['thread_inherit'] + thread_security = [0,0,0].pack('LLL') + thread_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE) + thread_security[8,4] = [1].pack('L') # TRUE + end + + # Automatically handle stdin, stdout and stderr as either IO objects + # or file descriptors. This won't work for StringIO, however. + ['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 + raise Error, get_last_error + 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 Error, get_last_error unless bool + + si_hash[io] = handle + si_hash['startf_flags'] ||= 0 + si_hash['startf_flags'] |= STARTF_USESTDHANDLES + hash['inherit'] = true + end + } + + # The bytes not covered here are reserved (null) + unless si_hash.empty? + startinfo[0,4] = [startinfo.size].pack('L') + startinfo[8,4] = [si_hash['desktop']].pack('p*') if si_hash['desktop'] + startinfo[12,4] = [si_hash['title']].pack('p*') if si_hash['title'] + startinfo[16,4] = [si_hash['x']].pack('L') if si_hash['x'] + startinfo[20,4] = [si_hash['y']].pack('L') if si_hash['y'] + startinfo[24,4] = [si_hash['x_size']].pack('L') if si_hash['x_size'] + startinfo[28,4] = [si_hash['y_size']].pack('L') if si_hash['y_size'] + startinfo[32,4] = [si_hash['x_count_chars']].pack('L') if si_hash['x_count_chars'] + startinfo[36,4] = [si_hash['y_count_chars']].pack('L') if si_hash['y_count_chars'] + startinfo[40,4] = [si_hash['fill_attribute']].pack('L') if si_hash['fill_attribute'] + startinfo[44,4] = [si_hash['startf_flags']].pack('L') if si_hash['startf_flags'] + startinfo[48,2] = [si_hash['sw_flags']].pack('S') if si_hash['sw_flags'] + startinfo[56,4] = [si_hash['stdin']].pack('L') if si_hash['stdin'] + startinfo[60,4] = [si_hash['stdout']].pack('L') if si_hash['stdout'] + startinfo[64,4] = [si_hash['stderr']].pack('L') if si_hash['stderr'] + end + + if hash['with_logon'] + logon = multi_to_wide(hash['with_logon']) + domain = multi_to_wide(hash['domain']) + app = hash['app_name'].nil? ? nil : multi_to_wide(hash['app_name']) + cmd = hash['command_line'].nil? ? nil : multi_to_wide(hash['command_line']) + cwd = multi_to_wide(hash['cwd']) + passwd = multi_to_wide(hash['password']) + + hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT + + process_ran = 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 + ) + else + process_ran = CreateProcess( + hash['app_name'], # App name + hash['command_line'], # Command line + process_security, # Process attributes + thread_security, # Thread attributes + hash['inherit'], # Inherit handles? + hash['creation_flags'], # Creation flags + env, # Environment + hash['cwd'], # Working directory + startinfo, # Startup Info + procinfo # Process Info + ) + end + + # TODO: Close stdin, stdout and stderr handles in the si_hash unless + # they're pointing to one of the standard handles already. [Maybe] + if !process_ran + raise_last_error("CreateProcess()") + end + + # Automatically close the process and thread handles in the + # PROCESS_INFORMATION struct unless explicitly told not to. + if hash['close_handles'] + CloseHandle(procinfo[0,4].unpack('L').first) + CloseHandle(procinfo[4,4].unpack('L').first) + end + + ProcessInfo.new( + procinfo[0,4].unpack('L').first, # hProcess + procinfo[4,4].unpack('L').first, # hThread + procinfo[8,4].unpack('L').first, # hProcessId + procinfo[12,4].unpack('L').first # hThreadId + ) + end + + def self.raise_last_error(operation) + error_string = "#{operation} failed: #{get_last_error}" + last_error_code = GetLastError() + if ERROR_CODE_MAP.has_key?(last_error_code) + raise ERROR_CODE_MAP[last_error_code], error_string + else + raise Error, error_string + end + end + + # List from ruby/win32/win32.c + ERROR_CODE_MAP = { + ERROR_INVALID_FUNCTION => Errno::EINVAL, + ERROR_FILE_NOT_FOUND => Errno::ENOENT, + ERROR_PATH_NOT_FOUND => Errno::ENOENT, + ERROR_TOO_MANY_OPEN_FILES => Errno::EMFILE, + ERROR_ACCESS_DENIED => Errno::EACCES, + ERROR_INVALID_HANDLE => Errno::EBADF, + ERROR_ARENA_TRASHED => Errno::ENOMEM, + ERROR_NOT_ENOUGH_MEMORY => Errno::ENOMEM, + ERROR_INVALID_BLOCK => Errno::ENOMEM, + ERROR_BAD_ENVIRONMENT => Errno::E2BIG, + ERROR_BAD_FORMAT => Errno::ENOEXEC, + ERROR_INVALID_ACCESS => Errno::EINVAL, + ERROR_INVALID_DATA => Errno::EINVAL, + ERROR_INVALID_DRIVE => Errno::ENOENT, + ERROR_CURRENT_DIRECTORY => Errno::EACCES, + ERROR_NOT_SAME_DEVICE => Errno::EXDEV, + ERROR_NO_MORE_FILES => Errno::ENOENT, + ERROR_WRITE_PROTECT => Errno::EROFS, + ERROR_BAD_UNIT => Errno::ENODEV, + ERROR_NOT_READY => Errno::ENXIO, + ERROR_BAD_COMMAND => Errno::EACCES, + ERROR_CRC => Errno::EACCES, + ERROR_BAD_LENGTH => Errno::EACCES, + ERROR_SEEK => Errno::EIO, + ERROR_NOT_DOS_DISK => Errno::EACCES, + ERROR_SECTOR_NOT_FOUND => Errno::EACCES, + ERROR_OUT_OF_PAPER => Errno::EACCES, + ERROR_WRITE_FAULT => Errno::EIO, + ERROR_READ_FAULT => Errno::EIO, + ERROR_GEN_FAILURE => Errno::EACCES, + ERROR_LOCK_VIOLATION => Errno::EACCES, + ERROR_SHARING_VIOLATION => Errno::EACCES, + ERROR_WRONG_DISK => Errno::EACCES, + ERROR_SHARING_BUFFER_EXCEEDED => Errno::EACCES, +# ERROR_BAD_NETPATH => Errno::ENOENT, +# ERROR_NETWORK_ACCESS_DENIED => Errno::EACCES, +# ERROR_BAD_NET_NAME => Errno::ENOENT, + ERROR_FILE_EXISTS => Errno::EEXIST, + ERROR_CANNOT_MAKE => Errno::EACCES, + ERROR_FAIL_I24 => Errno::EACCES, + ERROR_INVALID_PARAMETER => Errno::EINVAL, + ERROR_NO_PROC_SLOTS => Errno::EAGAIN, + ERROR_DRIVE_LOCKED => Errno::EACCES, + ERROR_BROKEN_PIPE => Errno::EPIPE, + ERROR_DISK_FULL => Errno::ENOSPC, + ERROR_INVALID_TARGET_HANDLE => Errno::EBADF, + ERROR_INVALID_HANDLE => Errno::EINVAL, + ERROR_WAIT_NO_CHILDREN => Errno::ECHILD, + ERROR_CHILD_NOT_COMPLETE => Errno::ECHILD, + ERROR_DIRECT_ACCESS_HANDLE => Errno::EBADF, + ERROR_NEGATIVE_SEEK => Errno::EINVAL, + ERROR_SEEK_ON_DEVICE => Errno::EACCES, + ERROR_DIR_NOT_EMPTY => Errno::ENOTEMPTY, +# ERROR_DIRECTORY => Errno::ENOTDIR, + ERROR_NOT_LOCKED => Errno::EACCES, + ERROR_BAD_PATHNAME => Errno::ENOENT, + ERROR_MAX_THRDS_REACHED => Errno::EAGAIN, +# ERROR_LOCK_FAILED => Errno::EACCES, + ERROR_ALREADY_EXISTS => Errno::EEXIST, + ERROR_INVALID_STARTING_CODESEG => Errno::ENOEXEC, + ERROR_INVALID_STACKSEG => Errno::ENOEXEC, + ERROR_INVALID_MODULETYPE => Errno::ENOEXEC, + ERROR_INVALID_EXE_SIGNATURE => Errno::ENOEXEC, + ERROR_EXE_MARKED_INVALID => Errno::ENOEXEC, + ERROR_BAD_EXE_FORMAT => Errno::ENOEXEC, + ERROR_ITERATED_DATA_EXCEEDS_64k => Errno::ENOEXEC, + ERROR_INVALID_MINALLOCSIZE => Errno::ENOEXEC, + ERROR_DYNLINK_FROM_INVALID_RING => Errno::ENOEXEC, + ERROR_IOPL_NOT_ENABLED => Errno::ENOEXEC, + ERROR_INVALID_SEGDPL => Errno::ENOEXEC, + ERROR_AUTODATASEG_EXCEEDS_64k => Errno::ENOEXEC, + ERROR_RING2SEG_MUST_BE_MOVABLE => Errno::ENOEXEC, + ERROR_RELOC_CHAIN_XEEDS_SEGLIM => Errno::ENOEXEC, + ERROR_INFLOOP_IN_RELOC_CHAIN => Errno::ENOEXEC, + ERROR_FILENAME_EXCED_RANGE => Errno::ENOENT, + ERROR_NESTING_NOT_ALLOWED => Errno::EAGAIN, +# ERROR_PIPE_LOCAL => Errno::EPIPE, + ERROR_BAD_PIPE => Errno::EPIPE, + ERROR_PIPE_BUSY => Errno::EAGAIN, + ERROR_NO_DATA => Errno::EPIPE, + ERROR_PIPE_NOT_CONNECTED => Errno::EPIPE, + ERROR_OPERATION_ABORTED => Errno::EINTR, +# ERROR_NOT_ENOUGH_QUOTA => Errno::ENOMEM, + ERROR_MOD_NOT_FOUND => Errno::ENOENT, + WSAEINTR => Errno::EINTR, + WSAEBADF => Errno::EBADF, +# WSAEACCES => Errno::EACCES, + WSAEFAULT => Errno::EFAULT, + WSAEINVAL => Errno::EINVAL, + WSAEMFILE => Errno::EMFILE, + WSAEWOULDBLOCK => Errno::EWOULDBLOCK, + WSAEINPROGRESS => Errno::EINPROGRESS, + WSAEALREADY => Errno::EALREADY, + WSAENOTSOCK => Errno::ENOTSOCK, + WSAEDESTADDRREQ => Errno::EDESTADDRREQ, + WSAEMSGSIZE => Errno::EMSGSIZE, + WSAEPROTOTYPE => Errno::EPROTOTYPE, + WSAENOPROTOOPT => Errno::ENOPROTOOPT, + WSAEPROTONOSUPPORT => Errno::EPROTONOSUPPORT, + WSAESOCKTNOSUPPORT => Errno::ESOCKTNOSUPPORT, + WSAEOPNOTSUPP => Errno::EOPNOTSUPP, + WSAEPFNOSUPPORT => Errno::EPFNOSUPPORT, + WSAEAFNOSUPPORT => Errno::EAFNOSUPPORT, + WSAEADDRINUSE => Errno::EADDRINUSE, + WSAEADDRNOTAVAIL => Errno::EADDRNOTAVAIL, + WSAENETDOWN => Errno::ENETDOWN, + WSAENETUNREACH => Errno::ENETUNREACH, + WSAENETRESET => Errno::ENETRESET, + WSAECONNABORTED => Errno::ECONNABORTED, + WSAECONNRESET => Errno::ECONNRESET, + WSAENOBUFS => Errno::ENOBUFS, + WSAEISCONN => Errno::EISCONN, + WSAENOTCONN => Errno::ENOTCONN, + WSAESHUTDOWN => Errno::ESHUTDOWN, + WSAETOOMANYREFS => Errno::ETOOMANYREFS, +# WSAETIMEDOUT => Errno::ETIMEDOUT, + WSAECONNREFUSED => Errno::ECONNREFUSED, + WSAELOOP => Errno::ELOOP, + WSAENAMETOOLONG => Errno::ENAMETOOLONG, + WSAEHOSTDOWN => Errno::EHOSTDOWN, + WSAEHOSTUNREACH => Errno::EHOSTUNREACH, +# WSAEPROCLIM => Errno::EPROCLIM, +# WSAENOTEMPTY => Errno::ENOTEMPTY, + WSAEUSERS => Errno::EUSERS, + WSAEDQUOT => Errno::EDQUOT, + WSAESTALE => Errno::ESTALE, + WSAEREMOTE => Errno::EREMOTE + } + + module_function :create +end diff --git a/spec/mixlib/shellout/windows_spec.rb b/spec/mixlib/shellout/windows_spec.rb new file mode 100644 index 0000000000..be515be783 --- /dev/null +++ b/spec/mixlib/shellout/windows_spec.rb @@ -0,0 +1,283 @@ +require 'spec_helper' + +describe 'Mixlib::ShellOut::Windows', :windows_only do + + describe 'Utils' do + describe '.should_run_under_cmd?' do + subject { Mixlib::ShellOut::Windows::Utils.should_run_under_cmd?(command) } + + def self.with_command(_command, &example) + context "with command: #{_command}" do + let(:command) { _command } + it(&example) + end + end + + context 'when unquoted' do + with_command(%q{ruby -e 'prints "foobar"'}) { should_not be_true } + + # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4825574 + with_command(%q{"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\NETFX 4.0 Tools\gacutil.exe" /i "C:\Program Files (x86)\NUnit 2.6\bin\framework\nunit.framework.dll"}) { should_not be_true } + + with_command(%q{ruby -e 'exit 1' | ruby -e 'exit 0'}) { should be_true } + with_command(%q{ruby -e 'exit 1' > out.txt}) { should be_true } + with_command(%q{ruby -e 'exit 1' > out.txt 2>&1}) { should be_true } + with_command(%q{ruby -e 'exit 1' < in.txt}) { should be_true } + with_command(%q{ruby -e 'exit 1' || ruby -e 'exit 0'}) { should be_true } + with_command(%q{ruby -e 'exit 1' && ruby -e 'exit 0'}) { should be_true } + with_command(%q{@echo TRUE}) { should be_true } + + with_command(%q{echo %PATH%}) { should be_true } + with_command(%q{run.exe %A}) { should be_false } + with_command(%q{run.exe B%}) { should be_false } + with_command(%q{run.exe %A B%}) { should be_false } + with_command(%q{run.exe %A B% %PATH%}) { should be_true } + with_command(%q{run.exe %A B% %_PATH%}) { should be_true } + with_command(%q{run.exe %A B% %PATH_EXT%}) { should be_true } + with_command(%q{run.exe %A B% %1%}) { should be_false } + with_command(%q{run.exe %A B% %PATH1%}) { should be_true } + with_command(%q{run.exe %A B% %_PATH1%}) { should be_true } + + context 'when outside quotes' do + with_command(%q{ruby -e "exit 1" | ruby -e "exit 0"}) { should be_true } + with_command(%q{ruby -e "exit 1" > out.txt}) { should be_true } + with_command(%q{ruby -e "exit 1" > out.txt 2>&1}) { should be_true } + with_command(%q{ruby -e "exit 1" < in.txt}) { should be_true } + with_command(%q{ruby -e "exit 1" || ruby -e "exit 0"}) { should be_true } + with_command(%q{ruby -e "exit 1" && ruby -e "exit 0"}) { should be_true } + with_command(%q{@echo "TRUE"}) { should be_true } + + context 'with unclosed quote' do + with_command(%q{ruby -e "exit 1" | ruby -e "exit 0}) { should be_true } + with_command(%q{ruby -e "exit 1" > "out.txt}) { should be_true } + with_command(%q{ruby -e "exit 1" > "out.txt 2>&1}) { should be_true } + with_command(%q{ruby -e "exit 1" < "in.txt}) { should be_true } + with_command(%q{ruby -e "exit 1" || "ruby -e "exit 0"}) { should be_true } + with_command(%q{ruby -e "exit 1" && "ruby -e "exit 0"}) { should be_true } + with_command(%q{@echo "TRUE}) { should be_true } + + with_command(%q{echo "%PATH%}) { should be_true } + with_command(%q{run.exe "%A}) { should be_false } + with_command(%q{run.exe "B%}) { should be_false } + with_command(%q{run.exe "%A B%}) { should be_false } + with_command(%q{run.exe "%A B% %PATH%}) { should be_true } + with_command(%q{run.exe "%A B% %_PATH%}) { should be_true } + with_command(%q{run.exe "%A B% %PATH_EXT%}) { should be_true } + with_command(%q{run.exe "%A B% %1%}) { should be_false } + with_command(%q{run.exe "%A B% %PATH1%}) { should be_true } + with_command(%q{run.exe "%A B% %_PATH1%}) { should be_true } + end + end + end + + context 'when quoted' do + with_command(%q{run.exe "ruby -e 'exit 1' || ruby -e 'exit 0'"}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' > out.txt"}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' > out.txt 2>&1"}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' < in.txt"}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' || ruby -e 'exit 0'"}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' && ruby -e 'exit 0'"}) { should be_false } + with_command(%q{run.exe "%PATH%"}) { should be_true } + with_command(%q{run.exe "%A"}) { should be_false } + with_command(%q{run.exe "B%"}) { should be_false } + with_command(%q{run.exe "%A B%"}) { should be_false } + with_command(%q{run.exe "%A B% %PATH%"}) { should be_true } + with_command(%q{run.exe "%A B% %_PATH%"}) { should be_true } + with_command(%q{run.exe "%A B% %PATH_EXT%"}) { should be_true } + with_command(%q{run.exe "%A B% %1%"}) { should be_false } + with_command(%q{run.exe "%A B% %PATH1%"}) { should be_true } + with_command(%q{run.exe "%A B% %_PATH1%"}) { should be_true } + + context 'with unclosed quote' do + with_command(%q{run.exe "ruby -e 'exit 1' || ruby -e 'exit 0'}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' > out.txt}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' > out.txt 2>&1}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' < in.txt}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' || ruby -e 'exit 0'}) { should be_false } + with_command(%q{run.exe "ruby -e 'exit 1' && ruby -e 'exit 0'}) { should be_false } + with_command(%q{run.exe "%PATH%}) { should be_true } + with_command(%q{run.exe "%A}) { should be_false } + with_command(%q{run.exe "B%}) { should be_false } + with_command(%q{run.exe "%A B%}) { should be_false } + with_command(%q{run.exe "%A B% %PATH%}) { should be_true } + with_command(%q{run.exe "%A B% %_PATH%}) { should be_true } + with_command(%q{run.exe "%A B% %PATH_EXT%}) { should be_true } + with_command(%q{run.exe "%A B% %1%}) { should be_false } + with_command(%q{run.exe "%A B% %PATH1%}) { should be_true } + with_command(%q{run.exe "%A B% %_PATH1%}) { should be_true } + end + end + end + end + + # Caveat: Private API methods are subject to change without notice. + # Monkeypatch at your own risk. + context 'for private API methods' do + + describe '::IS_BATCH_FILE' do + subject { candidate =~ Mixlib::ShellOut::Windows::IS_BATCH_FILE } + + def self.with_candidate(_context, _options = {}, &example) + context "with #{_context}" do + let(:candidate) { _options[:candidate] } + it(&example) + end + end + + with_candidate('valid .bat file', :candidate => 'autoexec.bat') { should be_true } + with_candidate('valid .cmd file', :candidate => 'autoexec.cmd') { should be_true } + with_candidate('valid quoted .bat file', :candidate => '"C:\Program Files\autoexec.bat"') { should be_true } + with_candidate('valid quoted .cmd file', :candidate => '"C:\Program Files\autoexec.cmd"') { should be_true } + + with_candidate('invalid .bat file', :candidate => 'autoexecbat') { should_not be_true } + with_candidate('invalid .cmd file', :candidate => 'autoexeccmd') { should_not be_true } + with_candidate('bat in filename', :candidate => 'abattoir.exe') { should_not be_true } + with_candidate('cmd in filename', :candidate => 'parse_cmd.exe') { should_not be_true } + + with_candidate('invalid quoted .bat file', :candidate => '"C:\Program Files\autoexecbat"') { should_not be_true } + with_candidate('invalid quoted .cmd file', :candidate => '"C:\Program Files\autoexeccmd"') { should_not be_true } + with_candidate('quoted bat in filename', :candidate => '"C:\Program Files\abattoir.exe"') { should_not be_true } + with_candidate('quoted cmd in filename', :candidate => '"C:\Program Files\parse_cmd.exe"') { should_not be_true } + end + + describe '#command_to_run' do + subject { stubbed_shell_out.send(:command_to_run, cmd) } + + let(:stubbed_shell_out) { fail NotImplemented, 'Must declare let(:stubbed_shell_out)' } + let(:shell_out) { Mixlib::ShellOut.new(cmd) } + + let(:utils) { Mixlib::ShellOut::Windows::Utils } + let(:with_valid_exe_at_location) { lambda { |s| utils.stub!(:find_executable).and_return(executable_path) } } + let(:with_invalid_exe_at_location) { lambda { |s| utils.stub!(:find_executable).and_return(nil) } } + + context 'with empty command' do + let(:stubbed_shell_out) { shell_out } + let(:cmd) { ' ' } + + it 'should return with a nil executable' do + should eql([nil, cmd]) + end + end + + context 'with batch files' do + let(:stubbed_shell_out) { shell_out.tap(&with_valid_exe_at_location) } + let(:cmd_invocation) { "cmd /c \"#{cmd}\"" } + let(:cmd_exe) { "C:\\Windows\\system32\\cmd.exe" } + let(:cmd) { "#{executable_path}" } + + context 'with .bat file' do + let(:executable_path) { '"C:\Program Files\Application\Start.bat"' } + + # Examples taken from: https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4825574 + context 'with executable path enclosed in double quotes' do + + it 'should use specified batch file' do + should eql([cmd_exe, cmd_invocation]) + end + + context 'with arguments' do + let(:cmd) { "#{executable_path} arguments" } + + it 'should use specified executable' do + should eql([cmd_exe, cmd_invocation]) + end + end + + context 'with quoted arguments' do + let(:cmd) { "#{executable_path} /i \"C:\Program Files (x86)\NUnit 2.6\bin\framework\nunit.framework.dll\"" } + + it 'should use specified executable' do + should eql([cmd_exe, cmd_invocation]) + end + end + end + end + + context 'with .cmd file' do + let(:executable_path) { '"C:\Program Files\Application\Start.cmd"' } + + # Examples taken from: https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4825574 + context 'with executable path enclosed in double quotes' do + + it 'should use specified batch file' do + should eql([cmd_exe, cmd_invocation]) + end + + context 'with arguments' do + let(:cmd) { "#{executable_path} arguments" } + + it 'should use specified executable' do + should eql([cmd_exe, cmd_invocation]) + end + end + + context 'with quoted arguments' do + let(:cmd) { "#{executable_path} /i \"C:\Program Files (x86)\NUnit 2.6\bin\framework\nunit.framework.dll\"" } + + it 'should use specified executable' do + should eql([cmd_exe, cmd_invocation]) + end + end + end + + end + end + + context 'with valid executable at location' do + let(:stubbed_shell_out) { shell_out.tap(&with_valid_exe_at_location) } + + context 'with executable path' do + let(:cmd) { "#{executable_path}" } + let(:executable_path) { 'C:\RUBY192\bin\ruby.exe' } + + it 'should use specified executable' do + should eql([executable_path, cmd]) + end + + context 'with arguments' do + let(:cmd) { "#{executable_path} arguments" } + + it 'should use specified executable' do + should eql([executable_path, cmd]) + end + end + + context 'with quoted arguments' do + let(:cmd) { "#{executable_path} -e \"print 'fee fie foe fum'\"" } + + it 'should use specified executable' do + should eql([executable_path, cmd]) + end + end + end + + # Examples taken from: https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4825574 + context 'with executable path enclosed in double quotes' do + let(:cmd) { "#{executable_path}" } + let(:executable_path) { '"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\NETFX 4.0 Tools\gacutil.exe"' } + + it 'should use specified executable' do + should eql([executable_path, cmd]) + end + + context 'with arguments' do + let(:cmd) { "#{executable_path} arguments" } + + it 'should use specified executable' do + should eql([executable_path, cmd]) + end + end + + context 'with quoted arguments' do + let(:cmd) { "#{executable_path} /i \"C:\Program Files (x86)\NUnit 2.6\bin\framework\nunit.framework.dll\"" } + + it 'should use specified executable' do + should eql([executable_path, cmd]) + end + end + end + + end + end + end +end diff --git a/spec/mixlib/shellout/shellout_spec.rb b/spec/mixlib/shellout_spec.rb index 0f08c62115..cc59829464 100644 --- a/spec/mixlib/shellout/shellout_spec.rb +++ b/spec/mixlib/shellout_spec.rb @@ -15,6 +15,11 @@ describe Mixlib::ShellOut do let(:ruby_code) { raise 'define let(:ruby_code)' } let(:options) { nil } + # On some testing environments, we have gems that creates a deprecation notice sent + # out on STDERR. To fix that, we disable gems on Ruby 1.9.2 + let(:ruby_eval) { lambda { |code| "ruby #{disable_gems} -e '#{code}'" } } + let(:disable_gems) { ( ruby_19? ? '--disable-gems' : '') } + context 'when instantiating' do subject { shell_cmd } let(:cmd) { 'apt-get install chef' } @@ -31,6 +36,7 @@ describe Mixlib::ShellOut do its(:timeout) { should eql(600) } its(:valid_exit_codes) { should eql([0]) } its(:live_stream) { should be_nil } + its(:input) { should be_nil } it "should set default environmental variables" do shell_cmd.environment.should == {"LC_ALL" => "C"} @@ -66,7 +72,7 @@ describe Mixlib::ShellOut do let(:expected_uid) { user_info.uid } let(:user_info) { Etc.getpwent } - it "should compute the uid of the user", :unix_only => true do + it "should compute the uid of the user", :unix_only do shell_cmd.uid.should eql(expected_uid) end end @@ -94,7 +100,7 @@ describe Mixlib::ShellOut do let(:expected_gid) { group_info.gid } let(:group_info) { Etc.getgrent } - it "should compute the gid of the user", :unix_only => true do + it "should compute the gid of the user", :unix_only do shell_cmd.gid.should eql(expected_gid) end end @@ -155,12 +161,22 @@ describe Mixlib::ShellOut do should eql(value) end end + + context 'when setting an input' do + let(:accessor) { :input } + let(:value) { "Random content #{rand(1000000)}" } + + it "should set the input" do + should eql(value) + end + end end context "with options hash" do let(:cmd) { 'brew install couchdb' } let(:options) { { :cwd => cwd, :user => user, :group => group, :umask => umask, - :timeout => timeout, :environment => environment, :returns => valid_exit_codes, :live_stream => stream } } + :timeout => timeout, :environment => environment, :returns => valid_exit_codes, + :live_stream => stream, :input => input } } let(:cwd) { '/tmp' } let(:user) { 'toor' } @@ -170,6 +186,7 @@ describe Mixlib::ShellOut do let(:environment) { { 'RUBY_OPTS' => '-w' } } let(:valid_exit_codes) { [ 0, 1, 42 ] } let(:stream) { StringIO.new } + let(:input) { 1.upto(10).map { "Data #{rand(100000)}" }.join("\n") } it "should set the working directory" do shell_cmd.cwd.should eql(cwd) @@ -229,6 +246,10 @@ describe Mixlib::ShellOut do shell_cmd.live_stream.should eql(stream) end + it "should set the input" do + shell_cmd.input.should eql(input) + end + context 'with an invalid option' do let(:options) { { :frab => :job } } let(:invalid_option_exception) { Mixlib::ShellOut::InvalidCommandOption } @@ -268,7 +289,6 @@ describe Mixlib::ShellOut do context 'when executing the command' do let(:dir) { Dir.mktmpdir } - let(:ruby_eval) { lambda { |code| "ruby -e '#{code}'" } } let(:dump_file) { "#{dir}/out.txt" } let(:dump_file_content) { stdout; IO.read(dump_file) } @@ -277,7 +297,7 @@ describe Mixlib::ShellOut do let(:fully_qualified_cwd) { File.expand_path(cwd) } let(:options) { { :cwd => cwd } } - context 'when running under Unix', :unix_only => true do + context 'when running under Unix', :unix_only do let(:cwd) { '/bin' } let(:cmd) { 'pwd' } @@ -286,7 +306,7 @@ describe Mixlib::ShellOut do end end - context 'when running under Windows', :windows_only => true do + context 'when running under Windows', :windows_only do let(:cwd) { Dir.tmpdir } let(:cmd) { 'echo %cd%' } @@ -319,7 +339,7 @@ describe Mixlib::ShellOut do context 'with LC_ALL set to nil' do let(:locale) { nil } - context 'when running under Unix', :unix_only => true do + context 'when running under Unix', :unix_only do let(:parent_locale) { ENV['LC_ALL'].to_s.strip } it "should use the parent process's locale" do @@ -327,7 +347,7 @@ describe Mixlib::ShellOut do end end - context 'when running under Windows', :windows_only => true do + context 'when running under Windows', :windows_only do # On windows, if an environmental variable is not set, it returns the key let(:parent_locale) { (ENV['LC_ALL'] || '%LC_ALL%').to_s.strip } @@ -349,20 +369,33 @@ describe Mixlib::ShellOut do end end + context "with an input" do + subject { stdout } + + let(:input) { 'hello' } + let(:ruby_code) { 'STDIN.sync = true; STDOUT.sync = true; puts gets' } + let(:options) { { :input => input } } + + it "should copy the input to the child's stdin" do + should eql("hello#{LINE_ENDING}") + end + end + context "when running different types of command" do + let(:script) { open_file.tap(&write_file).tap(&:close).tap(&make_executable) } + let(:file_name) { "#{dir}/Setup Script.cmd" } + let(:script_name) { "\"#{script.path}\"" } + + let(:open_file) { File.open(file_name, 'w') } + let(:write_file) { lambda { |f| f.write(script_content) } } + let(:make_executable) { lambda { |f| File.chmod(0755, f.path) } } + context 'with spaces in the path' do subject { chomped_stdout } let(:cmd) { script_name } - let(:script) { open_file.tap(&write_file).tap(&:close).tap(&make_executable) } - let(:file_name) { "#{dir}/blah blah.cmd" } - let(:script_name) { "\"#{script.path}\"" } - - let(:open_file) { File.open(file_name, 'w') } - let(:write_file) { lambda { |f| f.write(script_content) } } - let(:make_executable) { lambda { |f| File.chmod(0755, f.path) } } - context 'when running under Unix', :unix_only => true do + context 'when running under Unix', :unix_only do let(:script_content) { 'echo blah' } it 'should execute' do @@ -370,15 +403,45 @@ describe Mixlib::ShellOut do end end - context 'when running under Windows', :windows_only => true do - let(:script_content) { '@echo blah' } + context 'when running under Windows', :windows_only do + let(:cmd) { "#{script_name} #{argument}" } + let(:script_content) { '@echo %1' } + let(:argument) { rand(10000).to_s } it 'should execute' do - should eql('blah') + should eql(argument) + end + + context 'with multiple quotes in the command and args' do + context 'when using a batch file' do + let(:argument) { "\"Random #{rand(10000)}\"" } + + it 'should execute' do + should eql(argument) + end + end + + context 'when not using a batch file' do + let(:watch) { lambda { |a| ap a } } + let(:cmd) { "#{executable_file_name} #{script_name}" } + + let(:executable_file_name) { "\"#{dir}/Ruby Parser.exe\"".tap(&make_executable!) } + let(:make_executable!) { lambda { |filename| Mixlib::ShellOut.new("copy \"#{full_path_to_ruby}\" #{filename}").run_command } } + let(:script_content) { "print \"#{expected_output}\"" } + let(:expected_output) { "Random #{rand(10000)}" } + + let(:full_path_to_ruby) { ENV['PATH'].split(';').map(&try_ruby).reject(&:nil?).first } + let(:try_ruby) { lambda { |path| "#{path}\\ruby.exe" if File.executable? "#{path}\\ruby.exe" } } + + it 'should execute' do + should eql(expected_output) + end + end end end end + context 'with lots of long arguments' do subject { chomped_stdout } @@ -403,6 +466,7 @@ describe Mixlib::ShellOut do end end + context 'with backslashes' do subject { stdout } let(:backslashes) { %q{\\"\\\\} } @@ -427,7 +491,7 @@ describe Mixlib::ShellOut do end end - context 'with file pipes' do + context 'with stdout and stderr file pipes' do let(:code) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" } let(:cmd) { ruby_eval.call(code) + " > #{dump_file}" } @@ -444,6 +508,27 @@ describe Mixlib::ShellOut do end end + context 'with stdin file pipe' do + let(:code) { "STDIN.sync = true; STDOUT.sync = true; STDERR.sync = true; print gets; STDERR.print false" } + let(:cmd) { ruby_eval.call(code) + " < #{dump_file_path}" } + let(:file_content) { "Random content #{rand(100000)}" } + + let(:dump_file_path) { dump_file.path } + let(:dump_file) { open_file.tap(&write_file).tap(&:close) } + let(:file_name) { "#{dir}/input" } + + let(:open_file) { File.open(file_name, 'w') } + let(:write_file) { lambda { |f| f.write(file_content) } } + + it 'should execute' do + stdout.should eql(file_content) + end + + it 'should handle stderr' do + stderr.should eql('false') + end + end + context 'with stdout and stderr file pipes' do let(:code) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" } let(:cmd) { ruby_eval.call(code) + " > #{dump_file} 2>&1" } @@ -543,6 +628,19 @@ describe Mixlib::ShellOut do it "should set the exit status of the command" do exit_status.should eql(exit_code) end + + context 'with input data' do + let(:options) { { :returns => valid_exit_codes, :input => input } } + let(:input) { "Random data #{rand(1000000)}" } + + it "should raise ShellCommandFailed" do + lambda { executed_cmd.error! }.should raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "should set the exit status of the command" do + exit_status.should eql(exit_code) + end + end end context 'when exiting with invalid code 0' do @@ -622,6 +720,15 @@ describe Mixlib::ShellOut do end end + context 'with subprocess that closes stdin and continues writing to stdout' do + let(:ruby_code) { "STDIN.close; sleep 0.5; STDOUT.puts :win" } + let(:options) { { :input => "Random data #{rand(100000)}" } } + + it 'should not hang or lose outupt' do + stdout.should eql("win#{LINE_ENDING}") + end + end + context 'with subprocess that closes stdout and continues writing to stderr' do let(:ruby_code) { "STDOUT.close; sleep 0.5; STDERR.puts :win" } @@ -651,12 +758,27 @@ describe Mixlib::ShellOut do # over again and generate lots of garbage, which will not be collected # since we have to turn GC off to avoid segv. context 'with subprocess that closes STDOUT before closing STDERR' do - subject { unclosed_pipes } let(:ruby_code) { %q{STDOUT.puts "F" * 4096; STDOUT.close; sleep 0.1; STDERR.puts "foo"; STDERR.close; sleep 0.1; exit} } let(:unclosed_pipes) { executed_cmd.send(:open_pipes) } it 'should not hang' do - should be_empty + stdout.should_not be_empty + end + + it 'should close all pipes', :unix_only do + unclosed_pipes.should be_empty + end + end + + context 'with subprocess reading lots of data from stdin' do + subject { stdout.to_i } + let(:ruby_code) { 'STDOUT.print gets.size' } + let(:options) { { :input => input } } + let(:input) { 'f' * 20_000 } + let(:input_size) { input.size } + + it 'should not hang' do + should eql(input_size) end end @@ -682,6 +804,47 @@ describe Mixlib::ShellOut do end end + context 'with subprocess piping lots of data through stdin, stdout, and stderr' do + let(:multiplier) { 20 } + let(:expected_output_with) { lambda { |chr| (chr * multiplier) + (chr * multiplier) } } + + # Use regex to work across Ruby versions + let(:ruby_code) { "STDOUT.sync = STDERR.sync = true; while(input = gets) do ( input =~ /^f/ ? STDOUT : STDERR ).print input.chomp; end" } + + let(:options) { { :input => input } } + + context 'when writing to STDOUT first' do + let(:input) { [ 'f' * multiplier, 'u' * multiplier, 'f' * multiplier, 'u' * multiplier ].join(LINE_ENDING) } + + it "should not deadlock" do + stdout.should eql(expected_output_with.call('f')) + stderr.should eql(expected_output_with.call('u')) + end + end + + context 'when writing to STDERR first' do + let(:input) { [ 'u' * multiplier, 'f' * multiplier, 'u' * multiplier, 'f' * multiplier ].join(LINE_ENDING) } + + it "should not deadlock" do + stdout.should eql(expected_output_with.call('f')) + stderr.should eql(expected_output_with.call('u')) + end + end + end + + context 'when subprocess closes prematurely', :unix_only do + context 'with input data' do + let(:ruby_code) { 'bad_ruby { [ } ]' } + let(:options) { { :input => input } } + let(:input) { [ 'f' * 20_000, 'u' * 20_000, 'f' * 20_000, 'u' * 20_000 ].join(LINE_ENDING) } + + # Should the exception be handled? + it 'should raise error' do + lambda { executed_cmd }.should raise_error(Errno::EPIPE) + end + end + end + context 'when subprocess writes, pauses, then continues writing' do subject { stdout } let(:ruby_code) { %q{puts "before"; sleep 0.5; puts "after"} } @@ -700,12 +863,37 @@ describe Mixlib::ShellOut do end end + context 'when subprocess pauses before reading from stdin' do + subject { stdout.to_i } + let(:ruby_code) { 'sleep 0.5; print gets.size ' } + let(:input) { 'c' * 1024 } + let(:input_size) { input.size } + let(:options) { { :input => input } } + + it 'should not hang or lose output' do + should eql(input_size) + end + end + context 'when execution fails' do let(:cmd) { "fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu" } - it "should recover the error message" do - lambda { executed_cmd }.should raise_error(Errno::ENOENT) + context 'when running under Unix', :unix_only do + it "should recover the error message" do + lambda { executed_cmd }.should raise_error(Errno::ENOENT) + end + + context 'with input' do + let(:options) { {:input => input } } + let(:input) { "Random input #{rand(1000000)}" } + + it "should recover the error message" do + lambda { executed_cmd }.should raise_error(Errno::ENOENT) + end + end end + + pending 'when running under Windows', :windows_only end context 'without input data' do @@ -725,11 +913,11 @@ describe Mixlib::ShellOut do let(:ruby_code) { %q{STDERR.puts "msg_in_stderr"; puts "msg_in_stdout"} } let(:exception_output) { executed_cmd.format_for_exception.split("\n") } let(:expected_output) { [ - %q{---- Begin output of ruby -e 'STDERR.puts "msg_in_stderr"; puts "msg_in_stdout"' ----}, + "---- Begin output of #{cmd} ----", %q{STDOUT: msg_in_stdout}, %q{STDERR: msg_in_stderr}, - %q{---- End output of ruby -e 'STDERR.puts "msg_in_stderr"; puts "msg_in_stdout"' ----}, - "Ran ruby -e 'STDERR.puts \"msg_in_stderr\"; puts \"msg_in_stdout\"' returned 0" + "---- End output of #{cmd} ----", + "Ran #{cmd} returned 0" ] } it "should format exception messages" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6f400d9af7..0eab6ba4c1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,8 @@ require 'timeout' require 'ap' +WATCH = lambda { |x| ap x } unless defined?(WATCH) + # Load everything from spec/support # Do not change the gsub. Dir["spec/support/**/*.rb"].map { |f| f.gsub(%r{.rb$}, '') }.each { |f| require f } @@ -22,4 +24,5 @@ RSpec.configure do |config| config.filter_run_excluding :unix_only => true unless unix? config.run_all_when_everything_filtered = true + config.treat_symbols_as_metadata_keys_with_true_values = true end diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index 7721326450..69bdbf737e 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -1,4 +1,12 @@ +def ruby_19? + !!(RUBY_VERSION =~ /^1.9/) +end + +def ruby_18? + !!(RUBY_VERSION =~ /^1.8/) +end + def windows? !!(RUBY_PLATFORM =~ /mswin|mingw|windows/) end |