1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
|
# frozen_string_literal: true
require 'ffi'
require_relative '../../../../puppet/ffi/windows'
require_relative '../../../../puppet/util/windows/string'
module Process
extend FFI::Library
extend Puppet::Util::Windows::String
extend Puppet::FFI::Windows::APITypes
extend Puppet::FFI::Windows::Functions
extend Puppet::FFI::Windows::Structs
include Puppet::FFI::Windows::Constants
include Puppet::FFI::Windows::Structs
ProcessInfo = Struct.new(
'ProcessInfo',
:process_handle,
:thread_handle,
:process_id,
:thread_id
)
private_constant :ProcessInfo
# Disable popups. This mostly affects the Process.kill method.
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX)
class << self
private :SetHandleInformation, :SetErrorMode, :CreateProcessW, :OpenProcess,
:SetPriorityClass, :CreateProcessWithLogonW, :get_osfhandle, :get_errno
# Process.create(key => value, ...) => ProcessInfo
#
# This is a wrapper for the CreateProcess() function. It executes a process,
# returning a ProcessInfo struct. It accepts a hash as an argument.
# There are several primary keys:
#
# * command_line (this or app_name must be present)
# * app_name (default: nil)
# * inherit (default: false)
# * process_inherit (default: false)
# * thread_inherit (default: false)
# * creation_flags (default: 0)
# * cwd (default: Dir.pwd)
# * startup_info (default: nil)
# * environment (default: nil)
# * close_handles (default: true)
# * with_logon (default: nil)
# * domain (default: nil)
# * password (default: nil, mandatory if with_logon)
#
# Of these, the 'command_line' or 'app_name' must be specified or an
# error is raised. Both may be set individually, but 'command_line' should
# be preferred if only one of them is set because it does not (necessarily)
# require an explicit path or extension to work.
#
# The 'domain' and 'password' options are only relevent in the context
# of 'with_logon'. If 'with_logon' is set, then the 'password' option is
# mandatory.
#
# The startup_info key takes a hash. Its keys are attributes that are
# part of the StartupInfo struct, and are generally only meaningful for
# GUI or console processes. See the documentation on CreateProcess()
# and the StartupInfo struct on MSDN for more information.
#
# * desktop
# * title
# * x
# * y
# * x_size
# * y_size
# * x_count_chars
# * y_count_chars
# * fill_attribute
# * sw_flags
# * startf_flags
# * stdin
# * stdout
# * stderr
#
# Note that the 'stdin', 'stdout' and 'stderr' options can be either Ruby
# IO objects or file descriptors (i.e. a fileno). However, StringIO objects
# are not currently supported. Unfortunately, setting these is not currently
# an option for JRuby.
#
# If 'stdin', 'stdout' or 'stderr' are specified, then the +inherit+ value
# is automatically set to true and the Process::STARTF_USESTDHANDLES flag is
# automatically OR'd to the +startf_flags+ value.
#
# The ProcessInfo struct contains the following members:
#
# * process_handle - The handle to the newly created process.
# * thread_handle - The handle to the primary thread of the process.
# * process_id - Process ID.
# * thread_id - Thread ID.
#
# If the 'close_handles' option is set to true (the default) then the
# process_handle and the thread_handle are automatically closed for you
# before the ProcessInfo struct is returned.
#
# If the 'with_logon' option is set, then the process runs the specified
# executable file in the security context of the specified credentials.
VALID_KEYS = %i[
app_name command_line inherit creation_flags cwd environment
startup_info thread_inherit process_inherit close_handles with_logon
domain password
].freeze
VALID_SI_KEYS = %i[
startf_flags desktop title x y x_size y_size x_count_chars
y_count_chars fill_attribute sw_flags stdin stdout stderr
].freeze
private_constant :VALID_KEYS, :VALID_SI_KEYS
def create(args)
# Validate that args is a Hash
validate_args(args)
initialize_defaults
# Validate the keys, and convert symbols and case to lowercase strings.
validate_keys(args)
# If the startup_info key is present, validate its subkeys
validate_startup_info if hash[:startup_info]
# validates that 'app_name' or 'command_line' is set
validate_command_line
if hash[:app_name] && !hash[:command_line]
hash[:command_line] = hash[:app_name]
hash[:app_name] = nil
end
# Setup stdin, stdout and stderr handlers
setup_std_handlers
if logon
create_process_with_logon
else
create_process
end
# Automatically close the process and thread handles in the
# PROCESS_INFORMATION struct unless explicitly told not to.
if hash[:close_handles]
FFI::WIN32.CloseHandle(procinfo[:hProcess])
FFI::WIN32.CloseHandle(procinfo[:hThread])
end
ProcessInfo.new(
procinfo[:hProcess],
procinfo[:hThread],
procinfo[:dwProcessId],
procinfo[:dwThreadId]
)
end
remove_method :setpriority
# Sets the priority class for the specified process id +int+.
#
# The +kind+ parameter is ignored but present for API compatibility.
# You can only retrieve process information, not process group or user
# information, so it is effectively always Process::PRIO_PROCESS.
#
# Possible +int_priority+ values are:
#
# * Process::NORMAL_PRIORITY_CLASS
# * Process::IDLE_PRIORITY_CLASS
# * Process::HIGH_PRIORITY_CLASS
# * Process::REALTIME_PRIORITY_CLASS
# * Process::BELOW_NORMAL_PRIORITY_CLASS
# * Process::ABOVE_NORMAL_PRIORITY_CLASS
def setpriority(kind, int, int_priority)
raise TypeError unless kind.is_a?(Integer)
raise TypeError unless int.is_a?(Integer)
raise TypeError unless int_priority.is_a?(Integer)
int = Process.pid if int == 0
handle = OpenProcess(PROCESS_SET_INFORMATION, 0 , int)
if handle == 0
raise SystemCallError, FFI.errno, "OpenProcess"
end
begin
result = SetPriorityClass(handle, int_priority)
raise SystemCallError, FFI.errno, "SetPriorityClass" unless result
ensure
FFI::WIN32.CloseHandle(handle)
end
return 0
end
private
def initialize_defaults
@hash = {
app_name: nil,
creation_flags: 0,
close_handles: true
}
@si_hash = nil
@procinfo = nil
end
def validate_args(args)
raise TypeError, 'hash keyword arguments expected' unless args.is_a?(Hash)
end
def validate_keys(args)
args.each do |key, val|
key = key.to_s.to_sym
raise ArgumentError, "invalid key '#{key}'" unless VALID_KEYS.include?(key)
hash[key] = val
end
end
def validate_startup_info
hash[:startup_info].each do |key, val|
key = key.to_s.to_sym
raise ArgumentError, "invalid startup_info key '#{key}'" unless VALID_SI_KEYS.include?(key)
si_hash[key] = val
end
end
def validate_command_line
raise ArgumentError, 'command_line or app_name must be specified' unless hash[:app_name] || hash[:command_line]
end
def procinfo
@procinfo ||= PROCESS_INFORMATION.new
end
def hash
@hash ||= {}
end
def si_hash
@si_hash ||= {}
end
def app
wide_string(hash[:app_name])
end
def cmd
wide_string(hash[:command_line])
end
def cwd
wide_string(hash[:cwd])
end
def password
wide_string(hash[:password])
end
def logon
wide_string(hash[:with_logon])
end
def domain
wide_string(hash[:domain])
end
def env
env = hash[:environment]
return unless env
env = env.split(File::PATH_SEPARATOR) unless env.respond_to?(:join)
env = env.map { |e| e + 0.chr }.join('') + 0.chr
env = wide_string(env) if hash[:with_logon]
env
end
def process_security
return unless hash[:process_inherit]
process_security = SECURITY_ATTRIBUTES.new
process_security[:nLength] = SECURITY_ATTRIBUTES.size
process_security[:bInheritHandle] = 1
process_security
end
def thread_security
return unless hash[:thread_inherit]
thread_security = SECURITY_ATTRIBUTES.new
thread_security[:nLength] = SECURITY_ATTRIBUTES.size
thread_security[:bInheritHandle] = 1
thread_security
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.
def setup_std_handlers
%i[stdin stdout stderr].each do |io|
next unless si_hash[io]
handle = if si_hash[io].respond_to?(:fileno)
get_osfhandle(si_hash[io].fileno)
else
get_osfhandle(si_hash[io])
end
if handle == INVALID_HANDLE_VALUE
ptr = FFI::MemoryPointer.new(:int)
errno = if get_errno(ptr).zero?
ptr.read_int
else
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
def startinfo
startinfo = STARTUPINFO.new
return startinfo if 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]
startinfo
end
def create_process_with_logon
raise ArgumentError, 'password must be specified if with_logon is used' unless password
hash[:creation_flags] |= CREATE_UNICODE_ENVIRONMENT
bool = CreateProcessWithLogonW(
logon, # User
domain, # Domain
password, # 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
)
raise SystemCallError.new('CreateProcessWithLogonW', FFI.errno) unless bool
end
def create_process
inherit = hash[:inherit] ? 1 : 0
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
)
raise SystemCallError.new('CreateProcess', FFI.errno) unless bool
end
end
end
|