diff options
author | Daniel DeLeo <dan@opscode.com> | 2011-12-01 11:51:25 -0800 |
---|---|---|
committer | Daniel DeLeo <dan@opscode.com> | 2011-12-01 11:51:25 -0800 |
commit | 1f7734e92f30c0b6545bcc8b537e920e3f12b6bc (patch) | |
tree | 0677cc613ef17454cef35c9ce0ad07f50db28603 /lib | |
download | mixlib-shellout-1f7734e92f30c0b6545bcc8b537e920e3f12b6bc.tar.gz |
Initial extraction of ShellOut from Chef
Diffstat (limited to 'lib')
-rw-r--r-- | lib/mixlib/shellout.rb | 237 | ||||
-rw-r--r-- | lib/mixlib/shellout/exceptions.rb | 8 | ||||
-rw-r--r-- | lib/mixlib/shellout/unix.rb | 223 | ||||
-rw-r--r-- | lib/mixlib/shellout/version.rb | 5 | ||||
-rw-r--r-- | lib/mixlib/shellout/windows.rb | 554 |
5 files changed, 1027 insertions, 0 deletions
diff --git a/lib/mixlib/shellout.rb b/lib/mixlib/shellout.rb new file mode 100644 index 0000000..88a666d --- /dev/null +++ b/lib/mixlib/shellout.rb @@ -0,0 +1,237 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'etc' +require 'tmpdir' +require 'chef/log' +require 'fcntl' +require 'mixlib/shellout/exceptions' + +module Mixlib + + class ShellOut + READ_WAIT_TIME = 0.01 + READ_SIZE = 4096 + DEFAULT_READ_TIMEOUT = 600 + DEFAULT_ENVIRONMENT = {'LC_ALL' => 'C'} + + if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'mixlib/shellout/windows' + include ShellOut::Windows + else + require 'mixlib/shellout/unix' + include ShellOut::Unix + end + + attr_accessor :user + attr_accessor :group + attr_accessor :cwd + attr_accessor :valid_exit_codes + attr_accessor :live_stream + attr_accessor :command_log_level + attr_accessor :command_log_prepend + + attr_reader :command, :umask, :environment + attr_writer :timeout + attr_reader :execution_time + + attr_reader :stdout, :stderr, :status + + attr_reader :stdin_pipe, :stdout_pipe, :stderr_pipe, :process_status_pipe + + # === Arguments: + # Takes a single command, or a list of command fragments. These are used + # as arguments to Kernel.exec. See the Kernel.exec documentation for more + # explanation of how arguments are evaluated. The last argument can be an + # options Hash. + # === Options: + # If the last argument is a Hash, it is removed from the list of args passed + # to exec and used as an options hash. The following options are available: + # * +user+: the user the commmand should run as. if an integer is given, it is + # used as a uid. A string is treated as a username and resolved to a uid + # with Etc.getpwnam + # * +group+: the group the command should run as. works similarly to +user+ + # * +cwd+: the directory to chdir to before running the command + # * +umask+: a umask to set before running the command. If given as an Integer, + # be sure to use two leading zeros so it's parsed as Octal. A string will + # be treated as an octal integer + # * +returns+: one or more Integer values to use as valid exit codes for the + # subprocess. This only has an effect if you call +error!+ after + # +run_command+. + # * +environment+: a Hash of environment variables to set before the command + # is run. By default, the environment will *always* be set to 'LC_ALL' => 'C' + # to prevent issues with multibyte characters in Ruby 1.8. To avoid this, + # use :environment => nil for *no* extra environment settings, or + # :environment => {'LC_ALL'=>nil, ...} to set other environment settings + # without changing the locale. + # * +timeout+: a Numeric value for the number of seconds to wait on the + # child process before raising an Exception. This is calculated as the + # total amount of time that ShellOut waited on the child process without + # receiving any output (i.e., IO.select returned nil). Default is 60 + # seconds. Note: the stdlib Timeout library is not used. + # === Examples: + # Invoke find(1) to search for .rb files: + # find = Chef::ShellOut.new("find . -name '*.rb'") + # find.run_command + # # If all went well, the results are on +stdout+ + # puts find.stdout + # # find(1) prints diagnostic info to STDERR: + # puts "error messages" + find.stderr + # # Raise an exception if it didn't exit with 0 + # find.error! + # Run a command as the +www+ user with no extra ENV settings from +/tmp+ + # cmd = Chef::ShellOut.new("apachectl", "start", :user => 'www', :env => nil, :cwd => '/tmp') + # cmd.run_command # etc. + def initialize(*command_args) + @stdout, @stderr = '', '' + @live_stream = nil + @command_log_level = :debug + @command_log_prepend = nil + @environment = DEFAULT_ENVIRONMENT + @cwd = nil + @valid_exit_codes = [0] + + if command_args.last.is_a?(Hash) + parse_options(command_args.pop) + end + + @command = command_args.size == 1 ? command_args.first : command_args + end + + def umask=(new_umask) + @umask = (new_umask.respond_to?(:oct) ? new_umask.oct : new_umask.to_i) & 007777 + end + + def uid + return nil unless user + user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid + end + + def gid + return nil unless group + group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid + end + + def timeout + @timeout || DEFAULT_READ_TIMEOUT + end + + # Creates a String showing the output of the command, including a banner + # showing the exact command executed. Used by +invalid!+ to show command + # results when the command exited with an unexpected status. + def format_for_exception + msg = "" + msg << "---- Begin output of #{command} ----\n" + msg << "STDOUT: #{stdout.strip}\n" + msg << "STDERR: #{stderr.strip}\n" + msg << "---- End output of #{command} ----\n" + msg << "Ran #{command} returned #{status.exitstatus}" if status + msg + end + + def exitstatus + @status && @status.exitstatus + end + + # Run the command, writing the command's standard out and standard error + # to +stdout+ and +stderr+, and saving its exit status object to +status+ + # === Returns + # returns +self+; +stdout+, +stderr+, +status+, and +exitstatus+ will be + # populated with results of the command + # === Raises + # * Errno::EACCES when you are not privileged to execute the command + # * Errno::ENOENT when the command is not available on the system (or not + # in the current $PATH) + # * ::CommandTimeout when the command does not complete + # within +timeout+ seconds (default: 60s) + def run_command + if command_log_prepend + Chef::Log.send(command_log_level, "#{command_log_prepend} sh(#{@command})") + else + Chef::Log.send(command_log_level, "sh(#{@command})") + end + super + end + + # Checks the +exitstatus+ against the set of +valid_exit_codes+. If + # +exitstatus+ is not in the list of +valid_exit_codes+, calls +invalid!+, + # which raises an Exception. + # === Returns + # nil::: always returns nil when it does not raise + # === Raises + # ::ShellCommandFailed::: via +invalid!+ + def error! + unless Array(valid_exit_codes).include?(exitstatus) + invalid!("Expected process to exit with #{valid_exit_codes.inspect}, but received '#{exitstatus}'") + end + end + + # Raises a ::ShellCommandFailed exception, appending the + # command's stdout, stderr, and exitstatus to the exception message. + # === Arguments + # +msg+: A String to use as the basis of the exception message. The + # default explanation is very generic, providing a more informative message + # is highly encouraged. + # === Raises + # ::ShellCommandFailed always + def invalid!(msg=nil) + msg ||= "Command produced unexpected results" + raise ShellCommandFailed, msg + "\n" + format_for_exception + end + + def inspect + "<#{self.class.name}##{object_id}: command: '#@command' process_status: #{@status.inspect} " + + "stdout: '#{stdout.strip}' stderr: '#{stderr.strip}' child_pid: #{@child_pid.inspect} " + + "environment: #{@environment.inspect} timeout: #{timeout} user: #@user group: #@group working_dir: #@cwd >" + end + + private + + def parse_options(opts) + opts.each do |option, setting| + case option.to_s + when 'cwd' + self.cwd = setting + when 'user' + self.user = setting + when 'group' + self.group = setting + when 'umask' + self.umask = setting + when 'timeout' + self.timeout = setting + when 'returns' + self.valid_exit_codes = Array(setting) + when 'live_stream' + self.live_stream = setting + when 'command_log_level' + self.command_log_level = setting + when 'command_log_prepend' + self.command_log_prepend = setting + when 'environment', 'env' + # passing :environment => nil means don't set any new ENV vars + @environment = setting.nil? ? {} : @environment.dup.merge!(setting) + else + raise InvalidCommandOption, "option '#{option.inspect}' is not a valid option for #{self.class.name}" + end + end + end + + + end +end diff --git a/lib/mixlib/shellout/exceptions.rb b/lib/mixlib/shellout/exceptions.rb new file mode 100644 index 0000000..417def3 --- /dev/null +++ b/lib/mixlib/shellout/exceptions.rb @@ -0,0 +1,8 @@ +module Mixlib + class ShellOut + class ShellCommandFailed < RuntimeError; end + class CommandTimeout < RuntimeError; end + class InvalidCommandOption < RuntimeError; end + end +end + diff --git a/lib/mixlib/shellout/unix.rb b/lib/mixlib/shellout/unix.rb new file mode 100644 index 0000000..6e66b2a --- /dev/null +++ b/lib/mixlib/shellout/unix.rb @@ -0,0 +1,223 @@ +module Mixlib + class ShellOut + module Unix + + # Run the command, writing the command's standard out and standard error + # to +stdout+ and +stderr+, and saving its exit status object to +status+ + # === Returns + # returns +self+; +stdout+, +stderr+, +status+, and +exitstatus+ will be + # populated with results of the command + # === Raises + # * Errno::EACCES when you are not privileged to execute the command + # * Errno::ENOENT when the command is not available on the system (or not + # in the current $PATH) + # * Chef::Exceptions::CommandTimeout when the command does not complete + # within +timeout+ seconds (default: 60s) + def run_command + @child_pid = fork_subprocess + + configure_parent_process_file_descriptors + propagate_pre_exec_failure + + @result = nil + @execution_time = 0 + + # Ruby 1.8.7 and 1.8.6 from mid 2009 try to allocate objects during GC + # when calling IO.select and IO#read. Some OS Vendors are not interested + # in updating their ruby packages (Apple, *cough*) and we *have to* + # make it work. So I give you this epic hack: + GC.disable + until @status + ready = IO.select(open_pipes, nil, nil, READ_WAIT_TIME) + unless ready + @execution_time += READ_WAIT_TIME + if @execution_time >= timeout && !@result + raise CommandTimeout, "command timed out:\n#{format_for_exception}" + end + end + + if ready && ready.first.include?(child_stdout) + read_stdout_to_buffer + end + if ready && ready.first.include?(child_stderr) + read_stderr_to_buffer + end + + unless @status + # make one more pass to get the last of the output after the + # child process dies + if results = Process.waitpid2(@child_pid, Process::WNOHANG) + @status = results.last + redo + end + end + end + self + rescue Exception + # do our best to kill zombies + Process.waitpid2(@child_pid, Process::WNOHANG) rescue nil + raise + ensure + # no matter what happens, turn the GC back on, and hope whatever busted + # version of ruby we're on doesn't allocate some objects during the next + # GC run. + GC.enable + close_all_pipes + end + + private + + def set_user + if user + Process.euid = uid + Process.uid = uid + end + end + + def set_group + if group + Process.egid = gid + Process.gid = gid + end + end + + def set_environment + environment.each do |env_var,value| + ENV[env_var] = value + end + end + + def set_umask + File.umask(umask) if umask + end + + def set_cwd + Dir.chdir(cwd) if cwd + end + + def initialize_ipc + @stdout_pipe, @stderr_pipe, @process_status_pipe = IO.pipe, IO.pipe, IO.pipe + @process_status_pipe.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + end + + def child_stdout + @stdout_pipe[0] + end + + def child_stderr + @stderr_pipe[0] + end + + def child_process_status + @process_status_pipe[0] + end + + def close_all_pipes + child_stdout.close unless child_stdout.closed? + child_stderr.close unless child_stderr.closed? + child_process_status.close unless child_process_status.closed? + end + + # replace stdout, and stderr with pipes to the parent, and close the + # reader side of the error marshaling side channel. Close STDIN so when we + # exec, the new program will know it's never getting input ever. + def configure_subprocess_file_descriptors + process_status_pipe.first.close + + # HACK: for some reason, just STDIN.close isn't good enough when running + # under ruby 1.9.2, so make it good enough: + stdin_reader, stdin_writer = IO.pipe + stdin_writer.close + STDIN.reopen stdin_reader + stdin_reader.close + + stdout_pipe.first.close + STDOUT.reopen stdout_pipe.last + stdout_pipe.last.close + + stderr_pipe.first.close + STDERR.reopen stderr_pipe.last + stderr_pipe.last.close + + STDOUT.sync = STDERR.sync = true + end + + def configure_parent_process_file_descriptors + # Close the sides of the pipes we don't care about + stdout_pipe.last.close + stderr_pipe.last.close + process_status_pipe.last.close + # Get output as it happens rather than buffered + child_stdout.sync = true + child_stderr.sync = true + + true + end + + # Some patch levels of ruby in wide use (in particular the ruby 1.8.6 on OSX) + # segfault when you IO.select a pipe that's reached eof. Weak sauce. + def open_pipes + @open_pipes ||= [child_stdout, child_stderr] + end + + def read_stdout_to_buffer + while chunk = child_stdout.read_nonblock(READ_SIZE) + @stdout << chunk + @live_stream << chunk if @live_stream + end + rescue Errno::EAGAIN + rescue EOFError + open_pipes.delete_at(0) + end + + def read_stderr_to_buffer + while chunk = child_stderr.read_nonblock(READ_SIZE) + @stderr << chunk + end + rescue Errno::EAGAIN + rescue EOFError + open_pipes.delete_at(1) + end + + def fork_subprocess + initialize_ipc + + fork do + configure_subprocess_file_descriptors + + set_group + set_user + set_environment + set_umask + set_cwd + + begin + command.kind_of?(Array) ? exec(*command) : exec(command) + + raise 'forty-two' # Should never get here + rescue Exception => e + Marshal.dump(e, process_status_pipe.last) + process_status_pipe.last.flush + end + process_status_pipe.last.close unless (process_status_pipe.last.closed?) + exit! + end + end + + # Attempt to get a Marshaled error from the side-channel. + # If it's there, un-marshal it and raise. If it's not there, + # assume everything went well. + def propagate_pre_exec_failure + begin + e = Marshal.load child_process_status + raise(Exception === e ? e : "unknown failure: #{e.inspect}") + rescue EOFError # If we get an EOF error, then the exec was successful + true + ensure + child_process_status.close + end + end + + end + end +end diff --git a/lib/mixlib/shellout/version.rb b/lib/mixlib/shellout/version.rb new file mode 100644 index 0000000..1834e03 --- /dev/null +++ b/lib/mixlib/shellout/version.rb @@ -0,0 +1,5 @@ +module Mixlib + class Shellout + VERSION = "1.0.0.rc.0" + end +end diff --git a/lib/mixlib/shellout/windows.rb b/lib/mixlib/shellout/windows.rb new file mode 100644 index 0000000..7c79446 --- /dev/null +++ b/lib/mixlib/shellout/windows.rb @@ -0,0 +1,554 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'win32/process' +require 'windows/handle' +require 'windows/process' +require 'windows/synchronize' + +module Mixlib + class ShellOut + module Windows + + include ::Windows::Handle + include ::Windows::Process + include ::Windows::Synchronize + + TIME_SLICE = 0.05 + + #-- + # Missing lots of features from the UNIX version, such as + # uid, etc. + def run_command + + # + # Create pipes to capture stdout and stderr, + # + stdout_read, stdout_write = IO.pipe + stderr_read, stderr_write = IO.pipe + open_streams = [ stdout_read, stderr_read ] + + begin + + # + # Set cwd, environment, appname, etc. + # + app_name, command_line = command_to_run + create_process_args = { + :app_name => app_name, + :command_line => command_line, + :startup_info => { + :stdout => stdout_write, + :stderr => stderr_write + }, + :environment => inherit_environment.map { |k,v| "#{k}=#{v}" }, + :close_handles => false + } + create_process_args[:cwd] = cwd if cwd + + # + # Start the process + # + process = Process.create(create_process_args) + begin + + # + # Wait for the process to finish, consuming output as we go + # + start_wait = Time.now + while true + wait_status = WaitForSingleObject(process.process_handle, 0) + case wait_status + when WAIT_OBJECT_0 + # Get process exit code + exit_code = [0].pack('l') + unless GetExitCodeProcess(process.process_handle, exit_code) + raise get_last_error + end + @status = ThingThatLooksSortOfLikeAProcessStatus.new + @status.exitstatus = exit_code.unpack('l').first + + return self + when WAIT_TIMEOUT + # Kill the process + if (Time.now - start_wait) > timeout + raise Chef::Exceptions::CommandTimeout, "command timed out:\n#{format_for_exception}" + end + + consume_output(open_streams, stdout_read, stderr_read) + else + raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}" + end + + end + + ensure + CloseHandle(process.thread_handle) + CloseHandle(process.process_handle) + end + + ensure + # + # Consume all remaining data from the pipes until they are closed + # + stdout_write.close + stderr_write.close + + while consume_output(open_streams, stdout_read, stderr_read) + end + end + end + + private + + class ThingThatLooksSortOfLikeAProcessStatus + attr_accessor :exitstatus + end + + def consume_output(open_streams, stdout_read, stderr_read) + return false if open_streams.length == 0 + ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME) + return true if ! ready + + if ready.first.include?(stdout_read) + begin + next_chunk = stdout_read.readpartial(READ_SIZE) + @stdout << next_chunk + @live_stream << next_chunk if @live_stream + rescue EOFError + stdout_read.close + open_streams.delete(stdout_read) + end + end + + if ready.first.include?(stderr_read) + begin + @stderr << stderr_read.readpartial(READ_SIZE) + rescue EOFError + stderr_read.close + open_streams.delete(stderr_read) + end + end + + return true + end + + SHOULD_USE_CMD = /['"<>|&%]|\b(?:assoc|break|call|cd|chcp|chdir|cls|color|copy|ctty|date|del|dir|echo|endlocal|erase|exit|for|ftype|goto|if|lfnfor|lh|lock|md|mkdir|move|path|pause|popd|prompt|pushd|rd|rem|ren|rename|rmdir|set|setlocal|shift|start|time|title|truename|type|unlock|ver|verify|vol)\b/ + + def command_to_run + if command =~ SHOULD_USE_CMD + [ ENV['COMSPEC'], "cmd /c #{command}" ] + else + [ which(command[0,command.index(/\s/) || command.length]), command ] + end + end + + def inherit_environment + result = {} + ENV.each_pair do |k,v| + result[k] = v + end + + environment.each_pair do |k,v| + if v == nil + result.delete(k) + else + result[k] = v + end + end + result + end + + def which(cmd) + return cmd if File.executable? cmd + exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : [''] + ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each { |ext| + exe = "#{path}/#{cmd}#{ext}" + return exe if File.executable? exe + } + 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]) + 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 |