module Process
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 elevated
}
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 do |key, val|
key = key.to_s.downcase
unless valid_keys.include?(key)
raise ArgumentError, "invalid key '#{key}'"
end
hash[key] = val
end
si_hash = {}
# If the startup_info key is present, validate its subkeys
if hash["startup_info"]
hash["startup_info"].each do |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
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] = 1
end
# Thread SECURITY_ATTRIBUTE structure
thread_security = nil
if hash["thread_inherit"]
thread_security = SECURITY_ATTRIBUTES.new
thread_security[:nLength] = 12
thread_security[:bInheritHandle] = 1
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.
#
%w{stdin stdout stderr}.each do |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
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"] ? 1 : 0
if hash["with_logon"]
logon, passwd, domain = format_creds_from_hash(hash)
hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT
winsta_name = get_windows_station_name
# If running in the service windows station must do a log on to get
# to the interactive desktop. The 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 call with the LOGON32_LOGON_INTERACTIVE flag.
#
# User Access Control (UAC) only applies to interactive logons, so we
# can simulate running a command 'elevated' by running it under a separate
# logon as a batch process.
if hash["elevated"] || winsta_name =~ /^Service-0x0-.*$/i
logon_type = if hash["elevated"]
LOGON32_LOGON_BATCH
else
LOGON32_LOGON_INTERACTIVE
end
token = logon_user(logon, domain, passwd, logon_type)
create_process_as_user(token, app, cmd, process_security, thread_security, hash["creation_flags"], env, cwd, startinfo, procinfo)
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
)
unless bool
raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
end
end
else
bool = CreateProcessW(
app, # App name
cmd, # Command line
process_security, # Process attributes
thread_security, # Thread attributes
inherit, # Inherit handles?
hash["creation_flags"], # Creation flags
env, # Environment
cwd, # Working directory
startinfo, # Startup Info
procinfo # Process Info
)
unless bool
raise SystemCallError.new("CreateProcessW", FFI.errno)
end
end
# Automatically close the process and thread handles in the
# PROCESS_INFORMATION struct unless explicitly told not to.
if hash["close_handles"]
CloseHandle(procinfo[:hProcess])
CloseHandle(procinfo[:hThread])
# Clear these fields 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] = 0
procinfo[:hThread] = 0
end
ProcessInfo.new(
procinfo[:hProcess],
procinfo[:hThread],
procinfo[:dwProcessId],
procinfo[:dwThreadId]
)
end