require_relative "process/functions"
require_relative "process/constants"
require_relative "process/structs"
require_relative "process/helper"
module Process
include Process::Constants
extend Process::Functions
extend Process::Structs
extend Process::Constants
# The version of the win32-process library.
WIN32_PROCESS_VERSION = "1.0.0"
# Disable popups. This mostly affects the Process.kill method.
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX)
class << self
# Returns whether or not the current process is part of a Job (process group).
def job?
pbool = FFI::MemoryPointer.new(:int)
IsProcessInJob(GetCurrentProcess(), nil, pbool)
pbool.read_int == 1 ? true : false
end
# Returns the process and system affinity mask for the given +pid+, or the
# current process if no pid is provided. The return value is a two element
# array, with the first containing the process affinity mask, and the second
# containing the system affinity mask. Both are decimal values.
#
# A process affinity mask is a bit vector indicating the processors that a
# process is allowed to run on. A system affinity mask is a bit vector in
# which each bit represents the processors that are configured into a
# system.
#
# Example:
#
# # System has 4 processors, current process is allowed to run on all.
# Process.get_affinity # => [[15], [15]], where '15' is 1 + 2 + 4 + 8
#
# # System has 4 processors, current process only allowed on 1 and 4.
# Process.get_affinity # => [[9], [15]]
#
# If you want to convert a decimal bit vector into an array of 0's and 1's
# indicating the flag value of each processor, you can use something like
# this approach:
#
# mask = Process.get_affinity.first
# (0..mask).to_a.map{ |n| mask[n] }
#
def get_affinity(int = Process.pid)
pmask = FFI::MemoryPointer.new(:ulong)
smask = FFI::MemoryPointer.new(:ulong)
if int == Process.pid
unless GetProcessAffinityMask(GetCurrentProcess(), pmask, smask)
raise SystemCallError, FFI.errno, "GetProcessAffinityMask"
end
else
begin
handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , int)
if handle == 0
raise SystemCallError, FFI.errno, "OpenProcess"
end
unless GetProcessAffinityMask(handle, pmask, smask)
raise SystemCallError, FFI.errno, "GetProcessAffinityMask"
end
ensure
CloseHandle(handle)
end
end
[pmask.read_ulong, smask.read_ulong]
end
remove_method :getpriority
# Retrieves the priority class for the specified process id +int+. Unlike
# the default implementation, lower return values do not necessarily
# correspond to higher priority classes.
#
# The +kind+ parameter is ignored but required for API compatibility.
# You can only retrieve process information, not process group or user
# information, so it is effectively always Process::PRIO_PROCESS.
#
# Possible return values are:
#
# 32 => Process::NORMAL_PRIORITY_CLASS
# 64 => Process::IDLE_PRIORITY_CLASS
# 128 => Process::HIGH_PRIORITY_CLASS
# 256 => Process::REALTIME_PRIORITY_CLASS
# 16384 => Process::BELOW_NORMAL_PRIORITY_CLASS
# 32768 => Process::ABOVE_NORMAL_PRIORITY_CLASS
#
def getpriority(kind, int)
raise TypeError, kind unless kind.is_a?(Integer) # Match spec
raise TypeError, int unless int.is_a?(Integer) # Match spec
int = Process.pid if int == 0 # Match spec
handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, int)
if handle == 0
raise SystemCallError, FFI.errno, "OpenProcess"
end
begin
priority = GetPriorityClass(handle)
if priority == 0
raise SystemCallError, FFI.errno, "GetPriorityClass"
end
ensure
CloseHandle(handle)
end
priority
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) # Match spec
raise TypeError unless int.is_a?(Integer) # Match spec
raise TypeError unless int_priority.is_a?(Integer) # Match spec
int = Process.pid if int == 0 # Match spec
handle = OpenProcess(PROCESS_SET_INFORMATION, 0 , int)
if handle == 0
raise SystemCallError, FFI.errno, "OpenProcess"
end
begin
unless SetPriorityClass(handle, int_priority)
raise SystemCallError, FFI.errno, "SetPriorityClass"
end
ensure
CloseHandle(handle)
end
0 # Match the spec
end
remove_method :uid
# Returns the uid of the current process. Specifically, it returns the
# RID of the SID associated with the owner of the process.
#
# If +sid+ is set to true, then a binary sid is returned. Otherwise, a
# numeric id is returned (the default).
#--
# The Process.uid method in core Ruby always returns 0 on MS Windows.
#
def uid(sid = false)
token = FFI::MemoryPointer.new(:ulong)
raise TypeError unless sid.is_a?(TrueClass) || sid.is_a?(FalseClass)
unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
raise SystemCallError, FFI.errno, "OpenProcessToken"
end
token = token.read_ulong
rlength = FFI::MemoryPointer.new(:ulong)
tuser = 0.chr * 512
bool = GetTokenInformation(
token,
TokenUser,
tuser,
tuser.size,
rlength
)
unless bool
raise SystemCallError, FFI.errno, "GetTokenInformation"
end
string_sid = tuser[FFI.type_size(:pointer) * 2, (rlength.read_ulong - FFI.type_size(:pointer) * 2)]
if sid
string_sid
else
psid = FFI::MemoryPointer.new(:uintptr_t)
unless ConvertSidToStringSidA(string_sid, psid)
raise SystemCallError, FFI.errno, "ConvertSidToStringSid"
end
psid.read_pointer.read_string.split("-").last.to_i
end
end
remove_method :getrlimit
# Gets the resource limit of the current process. Only a limited number
# of flags are supported.
#
# Process::RLIMIT_CPU
# Process::RLIMIT_FSIZE
# Process::RLIMIT_AS
# Process::RLIMIT_RSS
# Process::RLIMIT_VMEM
#
# The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
# all refer to the Process memory limit. The Process::RLIMIT_CPU constant
# refers to the per process user time limit. The Process::RLIMIT_FSIZE
# constant is hard coded to the maximum file size on an NTFS filesystem,
# approximately 4TB (or 4GB if not NTFS).
#
# While a two element array is returned in order to comply with the spec,
# there is no separate hard and soft limit. The values will always be the
# same.
#
# If [0,0] is returned then it means no limit has been set.
#
# Example:
#
# Process.getrlimit(Process::RLIMIT_VMEM) # => [0, 0]
#--
# NOTE: Both the getrlimit and setrlimit method use an at_exit handler
# to close a job handle. This is necessary because simply calling it
# at the end of the block, while marking it for closure, would also make
# it unavailable within the same process again since it would no longer
# be associated with the job. In other words, trying to call it more than
# once within the same program would fail.
#
def getrlimit(resource)
if resource == RLIMIT_FSIZE
if volume_type == "NTFS"
return ((1024**4) * 4) - (1024 * 64) # ~ 4TB
else
return (1024**3) * 4 # 4 GB
end
end
handle = nil
in_job = Process.job?
# Put the current process in a job if it's not already in one
if in_job && defined?(@win32_process_job_name)
handle = OpenJobObjectA(JOB_OBJECT_QUERY, 1, @win32_process_job_name)
raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
else
@win32_process_job_name = "ruby_" + Process.pid.to_s
handle = CreateJobObjectA(nil, @win32_process_job_name)
raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
end
begin
unless in_job
unless AssignProcessToJobObject(handle, GetCurrentProcess())
raise Error, get_last_error
end
end
ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
val = nil
# Set the LimitFlags member of the struct
case resource
when RLIMIT_CPU
ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
else
raise ArgumentError, "unsupported resource type: '#{resource}'"
end
bool = QueryInformationJobObject(
handle,
JobObjectExtendedLimitInformation,
ptr,
ptr.size,
nil
)
unless bool
raise SystemCallError, FFI.errno, "QueryInformationJobObject"
end
case resource
when Process::RLIMIT_CPU
val = ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart]
when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
val = ptr[:ProcessMemoryLimit]
end
ensure
at_exit { CloseHandle(handle) if handle }
end
[val, val]
end
remove_method :setrlimit
# Sets the resource limit of the current process. Only a limited number
# of flags are supported.
#
# Process::RLIMIT_CPU
# Process::RLIMIT_AS
# Process::RLIMIT_RSS
# Process::RLIMIT_VMEM
#
# The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
# all refer to the Process memory limit. The Process::RLIMIT_CPU constant
# refers to the per process user time limit.
#
# The +max_limit+ parameter is provided for interface compatibility only.
# It is always set to the current_limit value.
#
# Example:
#
# Process.setrlimit(Process::RLIMIT_VMEM, 1024 * 4) # => nil
# Process.getrlimit(Process::RLIMIT_VMEM) # => [4096, 4096]
#
# WARNING: Exceeding the limit you set with this method could segfault
# the interpreter. Consider this method experimental.
#
def setrlimit(resource, current_limit, max_limit = nil)
max_limit = current_limit
handle = nil
in_job = Process.job?
unless [RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS, RLIMIT_CPU].include?(resource)
raise ArgumentError, "unsupported resource type: '#{resource}'"
end
# Put the current process in a job if it's not already in one
if in_job && defined? @win32_process_job_name
handle = OpenJobObjectA(JOB_OBJECT_SET_ATTRIBUTES, 1, @win32_process_job_name)
raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
else
@win32_process_job_name = "ruby_" + Process.pid.to_s
handle = CreateJobObjectA(nil, @win32_process_job_name)
raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
end
begin
unless in_job
unless AssignProcessToJobObject(handle, GetCurrentProcess())
raise SystemCallError, FFI.errno, "AssignProcessToJobObject"
end
end
ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
# Set the LimitFlags and relevant members of the struct
if resource == RLIMIT_CPU
ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = max_limit
else
ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
ptr[:ProcessMemoryLimit] = max_limit
end
bool = SetInformationJobObject(
handle,
JobObjectExtendedLimitInformation,
ptr,
ptr.size
)
unless bool
raise SystemCallError, FFI.errno, "SetInformationJobObject"
end
ensure
at_exit { CloseHandle(handle) if handle }
end
end
# 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.
#
# To simulate Process.wait you can use this approach:
#
# sleep 0.1 while !Process.get_exitcode(info.process_id)
#
# If you really to use Process.wait, then you should use the
# Process.spawn method instead of Process.create where possible.
#
def create(args)
unless args.is_a?(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] = 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 { |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
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
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
else
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
)
unless bool
raise SystemCallError.new("CreateProcess", 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])
end
ProcessInfo.new(
procinfo[:hProcess],
procinfo[:hThread],
procinfo[:dwProcessId],
procinfo[:dwThreadId]
)
end
remove_method :kill
# Kill a given process with a specific signal. This overrides the default
# implementation of Process.kill. The differences mainly reside in the way
# it kills processes, but this version also gives you finer control over
# behavior.
#
# Internally, signals 2 and 3 will generate a console control event, using
# a ctrl-c or ctrl-break event, respectively. Signal 9 terminates the
# process harshly, given that process no chance to do any internal cleanup.
# Signals 1 and 4-8 kill the process more nicely, giving the process a
# chance to do internal cleanup before being killed. Signal 0 behaves the
# same as the default implementation.
#
# When using signals 1 or 4-8 you may specify additional options that
# allow finer control over how that process is killed and how your program
# behaves.
#
# Possible options for signals 1 and 4-8.
#
# :exit_proc => The name of the exit function called when signal 1 or 4-8
# is used. The default is 'ExitProcess'.
#
# :dll_module => The name of the .dll (or .exe) that contains :exit_proc.
# The default is 'kernel32'.
#
# :wait_time => The time, in milliseconds, to wait for the process to
# actually die. The default is 5ms. If you specify 0 here
# then the process does not wait if the process is not
# signaled and instead returns immediately. Alternatively,
# you may specify Process::INFINITE, and your code will
# block until the process is actually signaled.
#
# Example:
#
# Process.kill(1, 12345, :exit_proc => 'ExitProcess', :module => 'kernel32')
#
def kill(signal, *pids)
# Match the spec, require at least 2 arguments
if pids.length == 0
raise ArgumentError, "wrong number of arguments (1 for at least 2)"
end
# Match the spec, signal may not be less than zero if numeric
if signal.is_a?(Numeric) && signal < 0 # EINVAL
raise SystemCallError.new(22)
end
# Match the spec, signal must be a numeric, string or symbol
unless signal.is_a?(String) || signal.is_a?(Numeric) || signal.is_a?(Symbol)
raise ArgumentError, "bad signal type #{signal.class}"
end
# Match the spec, making an exception for BRK/SIGBRK, if the signal name is invalid.
# Older versions of JRuby did not include KILL, so we've made an explicit exception
# for that here, too.
if signal.is_a?(String) || signal.is_a?(Symbol)
signal = signal.to_s.sub("SIG", "")
unless Signal.list.keys.include?(signal) || %w{KILL BRK}.include?(signal)
raise ArgumentError, "unsupported name '#{signal}'"
end
end
# If the last argument is a hash, pop it and assume it's a hash of options
if pids.last.is_a?(Hash)
hash = pids.pop
opts = {}
valid = %w{exit_proc dll_module wait_time}
hash.each { |k, v|
k = k.to_s.downcase
unless valid.include?(k)
raise ArgumentError, "invalid option '#{k}'"
end
opts[k] = v
}
exit_proc = opts["exit_proc"] || "ExitProcess"
dll_module = opts["dll_module"] || "kernel32"
wait_time = opts["wait_time"] || 5
else
wait_time = 5
exit_proc = "ExitProcess"
dll_module = "kernel32"
end
count = 0
pids.each { |pid|
raise TypeError unless pid.is_a?(Numeric) # Match spec, pid must be a number
raise SystemCallError.new(22) if pid < 0 # Match spec, EINVAL if pid less than zero
sigint = [Signal.list["INT"], "INT", "SIGINT", :INT, :SIGINT, 2]
# Match the spec
if pid == 0 && !sigint.include?(signal)
raise SystemCallError.new(22)
end
if signal == 0
access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
elsif signal == 9
access = PROCESS_TERMINATE
else
access = PROCESS_ALL_ACCESS
end
begin
handle = OpenProcess(access, 0, pid)
if signal != 0 && handle == 0
raise SystemCallError, FFI.errno, "OpenProcess"
end
case signal
when 0
if handle != 0
count += 1
else
if FFI.errno == ERROR_ACCESS_DENIED
count += 1
else
raise SystemCallError.new(3) # ESRCH
end
end
when Signal.list["INT"], "INT", "SIGINT", :INT, :SIGINT, 2
if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
count += 1
else
raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
end
when Signal.list["BRK"], "BRK", "SIGBRK", :BRK, :SIGBRK, 3
if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
count += 1
else
raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
end
when Signal.list["KILL"], "KILL", "SIGKILL", :KILL, :SIGKILL, 9
if TerminateProcess(handle, pid)
count += 1
else
raise SystemCallError.new("TerminateProcess", FFI.errno)
end
else
thread_id = FFI::MemoryPointer.new(:ulong)
mod = GetModuleHandle(dll_module)
if mod == 0
raise SystemCallError.new("GetModuleHandle: '#{dll_module}'", FFI.errno)
end
proc_addr = GetProcAddress(mod, exit_proc)
if proc_addr == 0
raise SystemCallError.new("GetProcAddress: '#{exit_proc}'", FFI.errno)
end
thread = CreateRemoteThread(handle, nil, 0, proc_addr, nil, 0, thread_id)
if thread > 0
WaitForSingleObject(thread, wait_time)
count += 1
else
raise SystemCallError.new("CreateRemoteThread", FFI.errno)
end
end
ensure
CloseHandle(handle) if handle
end
}
count
end
# Returns the exitcode of the process with given +pid+ or nil if the process
# is still running. Note that the process doesn't have to be a child process.
#
# This method is very handy for finding out if a process started with Process.create
# is still running. The usual way of calling Process.wait doesn't work when
# the process isn't recognized as a child process (ECHILD). This happens for example
# when stdin, stdout or stderr are set to custom values.
#
def get_exitcode(pid)
handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
if handle == INVALID_HANDLE_VALUE
raise SystemCallError.new("OpenProcess", FFI.errno)
end
begin
buf = FFI::MemoryPointer.new(:ulong, 1)
unless GetExitCodeProcess(handle, buf)
raise SystemCallError.new("GetExitCodeProcess", FFI.errno)
end
ensure
CloseHandle(handle)
end
exitcode = buf.read_int
if exitcode == STILL_ACTIVE
nil
else
exitcode
end
end
# Returns a list of process information structs in the form of a hash,
# with the pid as the key, and an array of information as the value of
# that key. The type of information in that array depends on the
# +info_type+ parameter. The possible values for +info_type+, and the
# type of information they each return is as follows:
#
# :thread => ThreadSnapInfo[:thread_id, :process_id, :base_priority]
# :heap => HeapSnapInfo[:address, :block_size, :flags, :process_id, :heap_id]
# :module => ModuleSnapInfo[:process_id, :address, :module_size, :handle, :name, :path]
# :process => ProcessSnapInfo[:process_id, :threads, :parent_process_id, :priority, :flags, :path]
#
# If no argument is provided, then :thread is assumed. Note that it is up
# to you to filter by pid if you wish.
#
# Example:
#
# # Get all thread info
# Process.snapshot.each{ |pid, v|
# puts "PID: #{pid}"
# p v
# }
#
# # Get module info for just the current process
# p Process.snapshot(:module)[Process.pid]
#
# # Get heap info for just the current process
# p Process.snapshot(:heap)[Process.pid]
#
# # Show pids of all running processes
# p Process.snapshot(:process).keys
#
def snapshot(info_type = "thread")
case info_type.to_s.downcase
when "thread"
flag = TH32CS_SNAPTHREAD
when "heap"
flag = TH32CS_SNAPHEAPLIST
when "module"
flag = TH32CS_SNAPMODULE
when "process"
flag = TH32CS_SNAPPROCESS
else
raise ArgumentError, "info_type '#{info_type}' unsupported"
end
begin
handle = CreateToolhelp32Snapshot(flag, Process.pid)
if handle == INVALID_HANDLE_VALUE
raise SystemCallError.new("CreateToolhelp32Snapshot", FFI.errno)
end
case info_type.to_s.downcase
when "thread"
array = get_thread_info(handle)
when "heap"
array = get_heap_info(handle)
when "module"
array = get_module_info(handle)
when "process"
array = get_process_info(handle)
end
array
ensure
CloseHandle(handle) if handle
end
end
end
class << self
private
# Private method that returns the volume type, e.g. "NTFS", etc.
def volume_type
buf = FFI::MemoryPointer.new(:char, 32)
bool = GetVolumeInformationA(nil, nil, 0, nil, nil, nil, buf, buf.size)
bool ? buf.read_string : nil
end
# Return thread info for Process.snapshot
def get_thread_info(handle, pid = nil)
lpte = THREADENTRY32.new
lpte[:dwSize] = lpte.size
hash = Hash.new { |h, k| h[k] = [] }
if Thread32First(handle, lpte)
hash[lpte[:th32OwnerProcessID]] << ThreadSnapInfo.new(lpte[:th32ThreadID], lpte[:th32OwnerProcessID], lpte[:tpBasePri])
else
if FFI.errno == ERROR_NO_MORE_FILES
return hash
else
raise SystemCallError.new("Thread32First", FFI.errno)
end
end
hash[lpte[:th32OwnerProcessID]] << ThreadSnapInfo.new(lpte[:th32ThreadID], lpte[:th32OwnerProcessID], lpte[:tpBasePri]) while Thread32Next(handle, lpte)
hash
end
# Return heap info for Process.snapshot
def get_heap_info(handle)
hash = Hash.new { |h, k| h[k] = [] }
hl = HEAPLIST32.new
hl[:dwSize] = hl.size
if Heap32ListFirst(handle, hl)
while Heap32ListNext(handle, hl)
he = HEAPENTRY32.new
he[:dwSize] = he.size
if Heap32First(he, Process.pid, hl[:th32HeapID])
hash[he[:th32ProcessID]] << HeapSnapInfo.new(he[:dwAddress], he[:dwBlockSize], he[:dwFlags], he[:th32ProcessID], he[:th32HeapID])
else
if FFI.errno == ERROR_NO_MORE_FILES
break
else
raise SystemCallError.new("Heap32First", FFI.errno)
end
end
hash[he[:th32ProcessID]] << HeapSnapInfo.new(he[:dwAddress], he[:dwBlockSize], he[:dwFlags], he[:th32ProcessID], he[:th32HeapID]) while Heap32Next(he)
end
end
hash
end
# Return module info for Process.snapshot
def get_module_info(handle)
hash = Hash.new { |h, k| h[k] = [] }
me = MODULEENTRY32.new
me[:dwSize] = me.size
if Module32First(handle, me)
hash[me[:th32ProcessID]] << ModuleSnapInfo.new(
me[:th32ProcessID],
me[:modBaseAddr].to_i,
me[:modBaseSize],
me[:hModule],
me[:szModule].to_s,
me[:szExePath].to_s
)
else
if FFI.errno == ERROR_NO_MORE_FILES
return hash
else
raise SystemCallError.new("Module32First", FFI.errno)
end
end
while Module32Next(handle, me)
hash[me[:th32ProcessID]] << ModuleSnapInfo.new(
me[:th32ProcessID],
me[:modBaseAddr].to_i,
me[:modBaseSize],
me[:hModule],
me[:szModule].to_s,
me[:szExePath].to_s
)
end
hash
end
# Return process info for Process.snapshot
def get_process_info(handle)
hash = Hash.new { |h, k| h[k] = [] }
pe = PROCESSENTRY32.new
pe[:dwSize] = pe.size
if Process32First(handle, pe)
hash[pe[:th32ProcessID]] = ProcessSnapInfo.new(
pe[:th32ProcessID],
pe[:cntThreads],
pe[:th32ParentProcessID],
pe[:pcPriClassBase],
pe[:dwFlags],
pe[:szExeFile].to_s
)
else
if FFI.errno == ERROR_NO_MORE_FILES
return hash
else
raise SystemCallError.new("Process32First", FFI.errno)
end
end
while Process32Next(handle, pe)
hash[pe[:th32ProcessID]] = ProcessSnapInfo.new(
pe[:th32ProcessID],
pe[:cntThreads],
pe[:th32ParentProcessID],
pe[:pcPriClassBase],
pe[:dwFlags],
pe[:szExeFile].to_s
)
end
hash
end
# Private method that returns the Windows major version number.
def windows_version
ver = OSVERSIONINFO.new
ver[:dwOSVersionInfoSize] = ver.size
unless GetVersionExA(ver)
raise SystemCallError.new("GetVersionEx", FFI.errno)
end
ver[:dwMajorVersion]
end
end
end