#--
# 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'
# Add new constants for Logon
module Process::Constants
LOGON32_LOGON_INTERACTIVE = 0x00000002
LOGON32_PROVIDER_DEFAULT = 0x00000000
UOI_NAME = 0x00000002
WAIT_OBJECT_0 = 0
WAIT_TIMEOUT = 0x102
WAIT_ABANDONED = 128
WAIT_ABANDONED_0 = WAIT_ABANDONED
WAIT_FAILED = 0xFFFFFFFF
end
# Define the functions needed to check with Service windows station
module Process::Functions
module FFI::Library
# Wrapper method for attach_function + private
def attach_pfunc(*args)
attach_function(*args)
private args[0]
end
end
extend FFI::Library
ffi_lib :advapi32
attach_pfunc :LogonUserW,
[:buffer_in, :buffer_in, :buffer_in, :ulong, :ulong, :pointer], :bool
attach_pfunc :CreateProcessAsUserW,
[:ulong, :buffer_in, :buffer_in, :pointer, :pointer, :bool,
:ulong, :buffer_in, :buffer_in, :pointer, :pointer], :bool
ffi_lib :user32
attach_pfunc :GetProcessWindowStation,
[], :ulong
attach_pfunc :GetUserObjectInformationA,
[:ulong, :uint, :buffer_out, :ulong, :pointer], :bool
end
# Override Process.create to check for running in the Service window station and doing
# a full logon with LogonUser, instead of a CreateProcessWithLogon
module Process
include Process::Constants
include Process::Structs
def create(args)
unless args.kind_of?(Hash)
raise TypeError, 'hash keyword arguments expected'
end
valid_keys = %w[
app_name command_line inherit creation_flags cwd environment
startup_info thread_inherit process_inherit close_handles with_logon
domain password
]
valid_si_keys = %w[
startf_flags desktop title x y x_size y_size x_count_chars
y_count_chars fill_attribute sw_flags stdin stdout stderr
]
# Set default values
hash = {
'app_name' => nil,
'creation_flags' => 0,
'close_handles' => true
}
# Validate the keys, and convert symbols and case to lowercase strings.
args.each{ |key, val|
key = key.to_s.downcase
unless valid_keys.include?(key)
raise ArgumentError, "invalid key '#{key}'"
end
hash[key] = val
}
si_hash = {}
# If the startup_info key is present, validate its subkeys
if hash['startup_info']
hash['startup_info'].each{ |key, val|
key = key.to_s.downcase
unless valid_si_keys.include?(key)
raise ArgumentError, "invalid startup_info key '#{key}'"
end
si_hash[key] = val
}
end
# The +command_line+ key is mandatory unless the +app_name+ key
# is specified.
unless hash['command_line']
if hash['app_name']
hash['command_line'] = hash['app_name']
hash['app_name'] = nil
else
raise ArgumentError, 'command_line or app_name must be specified'
end
end
env = nil
# The env string should be passed as a string of ';' separated paths.
if hash['environment']
env = hash['environment']
unless env.respond_to?(:join)
env = hash['environment'].split(File::PATH_SEPARATOR)
end
env = env.map{ |e| e + 0.chr }.join('') + 0.chr
env.to_wide_string! if hash['with_logon']
end
# Process SECURITY_ATTRIBUTE structure
process_security = nil
if hash['process_inherit']
process_security = SECURITY_ATTRIBUTES.new
process_security[:nLength] = 12
process_security[:bInheritHandle] = true
end
# Thread SECURITY_ATTRIBUTE structure
thread_security = nil
if hash['thread_inherit']
thread_security = SECURITY_ATTRIBUTES.new
thread_security[:nLength] = 12
thread_security[:bInheritHandle] = true
end
# Automatically handle stdin, stdout and stderr as either IO objects
# or file descriptors. This won't work for StringIO, however. It also
# will not work on JRuby because of the way it handles internal file
# descriptors.
#
['stdin', 'stdout', 'stderr'].each{ |io|
if si_hash[io]
if si_hash[io].respond_to?(:fileno)
handle = get_osfhandle(si_hash[io].fileno)
else
handle = get_osfhandle(si_hash[io])
end
if handle == INVALID_HANDLE_VALUE
ptr = FFI::MemoryPointer.new(:int)
if windows_version >= 6 && get_errno(ptr) == 0
errno = ptr.read_int
else
errno = FFI.errno
end
raise SystemCallError.new("get_osfhandle", errno)
end
# Most implementations of Ruby on Windows create inheritable
# handles by default, but some do not. RF bug #26988.
bool = SetHandleInformation(
handle,
HANDLE_FLAG_INHERIT,
HANDLE_FLAG_INHERIT
)
raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
si_hash[io] = handle
si_hash['startf_flags'] ||= 0
si_hash['startf_flags'] |= STARTF_USESTDHANDLES
hash['inherit'] = true
end
}
procinfo = PROCESS_INFORMATION.new
startinfo = STARTUPINFO.new
unless si_hash.empty?
startinfo[:cb] = startinfo.size
startinfo[:lpDesktop] = si_hash['desktop'] if si_hash['desktop']
startinfo[:lpTitle] = si_hash['title'] if si_hash['title']
startinfo[:dwX] = si_hash['x'] if si_hash['x']
startinfo[:dwY] = si_hash['y'] if si_hash['y']
startinfo[:dwXSize] = si_hash['x_size'] if si_hash['x_size']
startinfo[:dwYSize] = si_hash['y_size'] if si_hash['y_size']
startinfo[:dwXCountChars] = si_hash['x_count_chars'] if si_hash['x_count_chars']
startinfo[:dwYCountChars] = si_hash['y_count_chars'] if si_hash['y_count_chars']
startinfo[:dwFillAttribute] = si_hash['fill_attribute'] if si_hash['fill_attribute']
startinfo[:dwFlags] = si_hash['startf_flags'] if si_hash['startf_flags']
startinfo[:wShowWindow] = si_hash['sw_flags'] if si_hash['sw_flags']
startinfo[:cbReserved2] = 0
startinfo[:hStdInput] = si_hash['stdin'] if si_hash['stdin']
startinfo[:hStdOutput] = si_hash['stdout'] if si_hash['stdout']
startinfo[:hStdError] = si_hash['stderr'] if si_hash['stderr']
end
app = nil
cmd = nil
# Convert strings to wide character strings if present
if hash['app_name']
app = hash['app_name'].to_wide_string
end
if hash['command_line']
cmd = hash['command_line'].to_wide_string
end
if hash['cwd']
cwd = hash['cwd'].to_wide_string
end
inherit = hash['inherit'] || false
if hash['with_logon']
logon = hash['with_logon'].to_wide_string
if hash['password']
passwd = hash['password'].to_wide_string
else
raise ArgumentError, 'password must be specified if with_logon is used'
end
if hash['domain']
domain = hash['domain'].to_wide_string
end
hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
winsta_name = FFI::MemoryPointer.new(:char, 256)
return_size = FFI::MemoryPointer.new(:ulong)
bool = GetUserObjectInformationA(
GetProcessWindowStation(), # Window station handle
UOI_NAME, # Information to get
winsta_name, # Buffer to receive information
winsta_name.size, # Size of buffer
return_size # Size filled into buffer
)
unless bool
raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
end
winsta_name = winsta_name.read_string(return_size.read_ulong)
# If running in the service windows station must do a log on to get
# to the interactive desktop. Running process user account must have
# the 'Replace a process level token' permission. This is necessary as
# the logon (which happens with CreateProcessWithLogon) must have an
# interactive windows station to attach to, which is created with the
# LogonUser cann with the LOGON32_LOGON_INTERACTIVE flag.
if winsta_name =~ /^Service-0x0-.*$/i
token = FFI::MemoryPointer.new(:ulong)
bool = LogonUserW(
logon, # User
domain, # Domain
passwd, # Password
LOGON32_LOGON_INTERACTIVE, # Logon Type
LOGON32_PROVIDER_DEFAULT, # Logon Provider
token # User token handle
)
unless bool
raise SystemCallError.new("LogonUserW", FFI.errno)
end
token = token.read_ulong
begin
bool = CreateProcessAsUserW(
token, # User token handle
app, # App name
cmd, # Command line
process_security, # Process attributes
thread_security, # Thread attributes
inherit, # Inherit handles
hash['creation_flags'], # Creation Flags
env, # Environment
cwd, # Working directory
startinfo, # Startup Info
procinfo # Process Info
)
ensure
CloseHandle(token)
end
unless bool
raise SystemCallError.new("CreateProcessAsUserW (You must hold the 'Replace a process level token' permission)", FFI.errno)
end
else
bool = CreateProcessWithLogonW(
logon, # User
domain, # Domain
passwd, # Password
LOGON_WITH_PROFILE, # Logon flags
app, # App name
cmd, # Command line
hash['creation_flags'], # Creation flags
env, # Environment
cwd, # Working directory
startinfo, # Startup Info
procinfo # Process Info
)
end
unless bool
raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
end
else
bool = CreateProcessW(
app, # App name
cmd, # Command line
process_security, # Process attributes
thread_security, # Thread attributes
inherit, # Inherit handles?
hash['creation_flags'], # Creation flags
env, # Environment
cwd, # Working directory
startinfo, # Startup Info
procinfo # Process Info
)
unless bool
raise SystemCallError.new("CreateProcessW", FFI.errno)
end
end
# Automatically close the process and thread handles in the
# PROCESS_INFORMATION struct unless explicitly told not to.
if hash['close_handles']
CloseHandle(procinfo[:hProcess]) if procinfo[:hProcess]
CloseHandle(procinfo[:hThread]) if procinfo[:hThread]
# Set fields to nil so callers don't attempt to close the handle
# which can result in the wrong handle being closed or an
# exception in some circumstances
procinfo[:hProcess] = nil
procinfo[:hThread] = nil
end
ProcessInfo.new(
procinfo[:hProcess],
procinfo[:hThread],
procinfo[:dwProcessId],
procinfo[:dwThreadId]
)
end
module_function :create
end