summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHo-Sheng Hsiao <hosheng.hsiao@gmail.com>2012-05-11 14:03:52 -0700
committerHo-Sheng Hsiao <hosheng.hsiao@gmail.com>2012-05-11 14:03:52 -0700
commita8960e798a4229eea315a455b503cd77fc0b55a3 (patch)
treeebcfdef48739abded29ded7931d5cae4281770e4
parent3a72a18a9151b160cea1e47f226fc45ba295ed8e (diff)
parente2ef96f211d8e38e844f90537abb9cd27921e187 (diff)
downloadchef-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.md8
-rw-r--r--lib/mixlib/shellout.rb8
-rw-r--r--lib/mixlib/shellout/exceptions.rb1
-rw-r--r--lib/mixlib/shellout/unix.rb35
-rw-r--r--lib/mixlib/shellout/windows.rb508
-rw-r--r--lib/mixlib/shellout/windows/core_ext.rb385
-rw-r--r--spec/mixlib/shellout/windows_spec.rb283
-rw-r--r--spec/mixlib/shellout_spec.rb (renamed from spec/mixlib/shellout/shellout_spec.rb)242
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/platform_helpers.rb8
10 files changed, 1047 insertions, 434 deletions
diff --git a/README.md b/README.md
index e6cdc65642..93baa671ca 100644
--- a/README.md
+++ b/README.md
@@ -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