Skip to content

Commit

Permalink
Process#stop: make sure the full process tree is killed (on Unix). See
Browse files Browse the repository at this point in the history
  • Loading branch information
jarib committed Jan 31, 2014
1 parent 04c9591 commit ef927e9
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 77 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -20,5 +20,6 @@ pkg
.rbx
Gemfile.lock
.ruby-version
.bundle

## PROJECT::SPECIFIC
2 changes: 2 additions & 0 deletions lib/childprocess/unix/fork_exec_process.rb
Expand Up @@ -18,6 +18,8 @@ def launch_process
end

@pid = fork {
::Process.setpgid 0, 0 # same process group as parent

if @cwd
Dir.chdir(@cwd)
end
Expand Down
8 changes: 2 additions & 6 deletions lib/childprocess/unix/posix_spawn_process.rb
Expand Up @@ -12,7 +12,6 @@ def launch_process
pid_ptr = FFI::MemoryPointer.new(:pid_t)
actions = Lib::FileActions.new
attrs = Lib::Attrs.new
flags = 0

if @io
if @io.stdout
Expand All @@ -34,11 +33,8 @@ def launch_process
actions.add_close fileno_for(writer)
end

if defined? Platform::POSIX_SPAWN_USEVFORK
flags |= Platform::POSIX_SPAWN_USEVFORK
end

attrs.flags = flags
attrs.flags |= Platform::POSIX_SPAWN_SETPGROUP
attrs.flags |= Platform::POSIX_SPAWN_USEVFORK if defined? Platform::POSIX_SPAWN_USEVFORK

# wrap in helper classes in order to avoid GC'ed pointers
argv = Argv.new(@args)
Expand Down
2 changes: 1 addition & 1 deletion lib/childprocess/unix/process.rb
Expand Up @@ -66,7 +66,7 @@ def send_signal(sig)
assert_started

log "sending #{sig}"
::Process.kill sig, @pid
::Process.kill sig, -@pid
end

end # Process
Expand Down
28 changes: 15 additions & 13 deletions lib/childprocess/windows/handle.rb
Expand Up @@ -23,23 +23,25 @@ def open(pid, access = PROCESS_ALL_ACCESS)
end
end

def initialize(handle, pid)
unless handle.kind_of?(FFI::Pointer)
raise TypeError, "invalid handle: #{handle.inspect}"
attr_reader :pointer

def initialize(pointer, pid)
unless pointer.kind_of?(FFI::Pointer)
raise TypeError, "invalid handle: #{pointer.inspect}"
end

if handle.null?
raise ArgumentError, "handle is null: #{handle.inspect}"
if pointer.null?
raise ArgumentError, "handle is null: #{pointer.inspect}"
end

@pid = pid
@handle = handle
@closed = false
@pid = pid
@pointer = pointer
@closed = false
end

def exit_code
code_pointer = FFI::MemoryPointer.new :ulong
ok = Lib.get_exit_code(@handle, code_pointer)
ok = Lib.get_exit_code(@pointer, code_pointer)

if ok
code_pointer.get_ulong(0)
Expand All @@ -58,14 +60,14 @@ def send(signal)
when WIN_SIGBREAK
Lib.generate_console_ctrl_event(CTRL_BREAK_EVENT, @pid)
when WIN_SIGKILL
ok = Lib.terminate_process(@handle, @pid)
ok = Lib.terminate_process(@pointer, @pid)
Lib.check_error ok
else
thread_id = FFI::MemoryPointer.new(:ulong)
module_handle = Lib.get_module_handle("kernel32")
proc_address = Lib.get_proc_address(module_handle, "ExitProcess")

thread = Lib.create_remote_thread(@handle, 0, 0, proc_address, 0, 0, thread_id)
thread = Lib.create_remote_thread(@pointer, 0, 0, proc_address, 0, 0, thread_id)
check_error thread

Lib.wait_for_single_object(thread, 5)
Expand All @@ -76,12 +78,12 @@ def send(signal)
def close
return if @closed

Lib.close_handle(@handle)
Lib.close_handle(@pointer)
@closed = true
end

def wait(milliseconds = nil)
Lib.wait_for_single_object(@handle, milliseconds || INFINITE)
Lib.wait_for_single_object(@pointer, milliseconds || INFINITE)
end

end # Handle
Expand Down
101 changes: 76 additions & 25 deletions lib/childprocess/windows/lib.rb
@@ -1,30 +1,34 @@
module ChildProcess
module Windows
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000

PROCESS_ALL_ACCESS = 0x1F0FFF
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
PROCESS_STILL_ACTIVE = 259
PROCESS_ALL_ACCESS = 0x1F0FFF
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
PROCESS_STILL_ACTIVE = 259

INFINITE = 0xFFFFFFFF
INFINITE = 0xFFFFFFFF

WIN_SIGINT = 2
WIN_SIGBREAK = 3
WIN_SIGKILL = 9
WIN_SIGINT = 2
WIN_SIGBREAK = 3
WIN_SIGKILL = 9

CTRL_C_EVENT = 0
CTRL_BREAK_EVENT = 1
CTRL_C_EVENT = 0
CTRL_BREAK_EVENT = 1

DETACHED_PROCESS = 0x00000008
DETACHED_PROCESS = 0x00000008

STARTF_USESTDHANDLES = 0x00000100
INVALID_HANDLE_VALUE = -1
HANDLE_FLAG_INHERIT = 0x00000001
STARTF_USESTDHANDLES = 0x00000100
INVALID_HANDLE_VALUE = -1
HANDLE_FLAG_INHERIT = 0x00000001

DUPLICATE_SAME_ACCESS = 0x00000002
CREATE_UNICODE_ENVIRONMENT = 0x00000400
DUPLICATE_SAME_ACCESS = 0x00000002
CREATE_UNICODE_ENVIRONMENT = 0x00000400

JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9
JOB_OBJECT_BASIC_LIMIT_INFORMATION = 2

module Lib
enum :wait_status, [
Expand Down Expand Up @@ -61,12 +65,6 @@ module Lib
:pointer,
:pointer], :bool

#
# DWORD WINAPI GetLastError(void);
#

attach_function :get_last_error, :GetLastError, [], :ulong

#
# DWORD WINAPI FormatMessage(
# __in DWORD dwFlags,
Expand Down Expand Up @@ -101,6 +99,35 @@ module Lib

attach_function :open_process, :OpenProcess, [:ulong, :bool, :ulong], :pointer

#
# HANDLE WINAPI CreateJobObject(
# _In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes,
# _In_opt_ LPCTSTR lpName
# );
#

attach_function :create_job_object, :CreateJobObjectA, [:pointer, :pointer], :pointer

#
# BOOL WINAPI AssignProcessToJobObject(
# _In_ HANDLE hJob,
# _In_ HANDLE hProcess
# );

attach_function :assign_process_to_job_object, :AssignProcessToJobObject, [:pointer, :pointer], :bool

#
# BOOL WINAPI SetInformationJobObject(
# _In_ HANDLE hJob,
# _In_ JOBOBJECTINFOCLASS JobObjectInfoClass,
# _In_ LPVOID lpJobObjectInfo,
# _In_ DWORD cbJobObjectInfoLength
# );
#

attach_function :set_information_job_object, :SetInformationJobObject, [:pointer, :int, :pointer, :ulong], :bool

#
#
# DWORD WINAPI WaitForSingleObject(
# __in HANDLE hHandle,
Expand Down Expand Up @@ -243,7 +270,8 @@ def dont_inherit(file)
end

def last_error_message
errnum = get_last_error
errnum = FFI.errno

buf = FFI::MemoryPointer.new :char, 512

size = format_message(
Expand All @@ -259,6 +287,18 @@ def last_error_message
end
end

def each_child_of(pid, &blk)
raise NotImplementedError

# http://stackoverflow.com/questions/1173342/terminate-a-process-tree-c-for-windows?rq=1

# for each process entry
# if pe.th32ParentProcessID == pid
# Handle.open(pe.pe.th32ProcessId, &blk)
# end
#
end

def handle_for(fd_or_io)
if fd_or_io.kind_of?(IO) || fd_or_io.respond_to?(:fileno)
if ChildProcess.jruby?
Expand Down Expand Up @@ -344,6 +384,17 @@ def check_error(bool)
bool or raise Error, last_error_message
end

def alive?(pid)
handle = Lib.open_process(PROCESS_ALL_ACCESS, false, pid)
if handle.null?
false
else
ptr = FFI::MemoryPointer.new :ulong
Lib.check_error Lib.get_exit_code(handle, ptr)
ptr.read_ulong == PROCESS_STILL_ACTIVE
end
end

def no_hang?(flags)
(flags & Process::WNOHANG) == Process::WNOHANG
end
Expand Down
39 changes: 38 additions & 1 deletion lib/childprocess/windows/process.rb
Expand Up @@ -11,13 +11,13 @@ def io
def stop(timeout = 3)
assert_started

# just kill right away on windows.
log "sending KILL"
@handle.send(WIN_SIGKILL)

poll_for_exit(timeout)
ensure
@handle.close
@job.close
end

def wait
Expand All @@ -27,6 +27,7 @@ def wait
@handle.wait
@exit_code = @handle.exit_code
@handle.close
@job.close

@exit_code
end
Expand Down Expand Up @@ -63,9 +64,12 @@ def launch_process
builder.stderr = @io.stderr
end

@job = Job.new
@pid = builder.start
@handle = Handle.open @pid

@job << @handle

if duplex?
raise Error, "no stdin stream" unless builder.stdin
io._stdin = builder.stdin
Expand All @@ -74,6 +78,39 @@ def launch_process
self
end

class Job
def initialize
@pointer = Lib.create_job_object(nil, nil)

if @pointer.nil? || @pointer.null?
raise Error, "unable to create job object"
end

basic = JobObjectBasicLimitInformation.new
basic[:LimitFlags] = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE

extended = JobObjectExtendedLimitInformation.new
extended[:BasicLimitInformation] = basic

ret = Lib.set_information_job_object(
@pointer,
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
extended,
extended.size
)

Lib.check_error ret
end

def <<(handle)
Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
end

def close
Lib.close_handle @pointer
end
end

end # Process
end # Windows
end # ChildProcess
1 change: 1 addition & 0 deletions lib/childprocess/windows/process_builder.rb
Expand Up @@ -17,6 +17,7 @@ def initialize(args)
@stdin = nil

@flags = 0
@job_ptr = nil
@cmd_ptr = nil
@env_ptr = nil
@cwd_ptr = nil
Expand Down

0 comments on commit ef927e9

Please sign in to comment.