Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add full stub for Windows signals #13131

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions spec/std/process_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -325,26 +325,28 @@ describe Process do
end
end

describe "#signal" do
pending_win32 "kills a process" do
process = Process.new(*standing_command)
process.signal(Signal::KILL).should be_nil
ensure
process.try &.wait
end
{% unless flag?(:win32) %}
describe "#signal(Signal::KILL)" do
it "kills a process" do
process = Process.new(*standing_command)
process.signal(Signal::KILL).should be_nil
ensure
process.try &.wait
end

pending_win32 "kills many process" do
process1 = Process.new(*standing_command)
process2 = Process.new(*standing_command)
process1.signal(Signal::KILL).should be_nil
process2.signal(Signal::KILL).should be_nil
ensure
process1.try &.wait
process2.try &.wait
it "kills many process" do
process1 = Process.new(*standing_command)
process2 = Process.new(*standing_command)
process1.signal(Signal::KILL).should be_nil
process2.signal(Signal::KILL).should be_nil
ensure
process1.try &.wait
process2.try &.wait
end
end
end
{% end %}

pending_win32 "#terminate" do
it "#terminate" do
process = Process.new(*standing_command)
process.exists?.should be_true
process.terminated?.should be_false
Expand All @@ -368,7 +370,7 @@ describe Process do
process.terminated?.should be_false

# Kill, zombie now
process.signal(Signal::KILL)
process.terminate
process.exists?.should be_true
process.terminated?.should be_false

Expand All @@ -381,7 +383,7 @@ describe Process do
pending_win32 ".pgid" do
process = Process.new(*standing_command)
Process.pgid(process.pid).should be_a(Int64)
process.signal(Signal::KILL)
process.terminate
Process.pgid.should eq(Process.pgid(Process.pid))
ensure
process.try(&.wait)
Expand Down
2 changes: 1 addition & 1 deletion src/crystal/system/file.cr
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ module Crystal::System::File

# Closes the internal file descriptor without notifying libevent.
# This is directly used after the fork of a process to close the
# parent's Crystal::Signal.@@pipe reference before re initializing
# parent's Crystal::System::Signal.@@pipe reference before re initializing
# the event loop. In the case of a fork that will exec there is even
# no need to initialize the event loop at all.
# def file_descriptor_close
Expand Down
20 changes: 20 additions & 0 deletions src/crystal/system/signal.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Crystal::System::Signal
# Sets the handler for this signal to the passed function.
# def self.trap(signal, handler) : Nil

# Resets the handler for this signal to the OS default.
# def self.reset(signal) : Nil

# Clears the handler for this signal and prevents the OS default action.
# def self.ignore(signal) : Nil
end

{% if flag?(:wasi) %}
require "./wasi/signal"
{% elsif flag?(:unix) %}
require "./unix/signal"
{% elsif flag?(:win32) %}
require "./win32/signal"
{% else %}
{% raise "No Crystal::System::Signal implementation available" %}
{% end %}
6 changes: 3 additions & 3 deletions src/crystal/system/unix/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct Crystal::System::Process
getter pid : LibC::PidT

def initialize(@pid : LibC::PidT)
@channel = Crystal::SignalChildHandler.wait(@pid)
@channel = Crystal::System::SignalChildHandler.wait(@pid)
end

def release
Expand Down Expand Up @@ -71,7 +71,7 @@ struct Crystal::System::Process
end

def self.start_interrupt_loop : Nil
# do nothing; `Crystal::Signal.start_loop` takes care of this
# do nothing; `Crystal::System::Signal.start_loop` takes care of this
end

def self.exists?(pid)
Expand Down Expand Up @@ -110,7 +110,7 @@ struct Crystal::System::Process
pid = nil
if will_exec
# reset signal handlers, then sigmask (inherited on exec):
Crystal::Signal.after_fork_before_exec
Crystal::System::Signal.after_fork_before_exec
LibC.sigemptyset(pointerof(newmask))
LibC.pthread_sigmask(LibC::SIG_SETMASK, pointerof(newmask), nil)
else
Expand Down
270 changes: 270 additions & 0 deletions src/crystal/system/unix/signal.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
require "c/signal"
require "c/stdio"
require "c/sys/wait"
require "c/unistd"

module Crystal::System::Signal
# The number of libc functions that can be called safely from a signal(2)
# handler is very limited. An usual safe solution is to use a pipe(2) and
# just write the signal to the file descriptor and nothing more. A loop in
# the main program is responsible for reading the signals back from the
# pipe(2) and handle the signal there.

alias Handler = ::Signal ->

@@pipe = IO.pipe(read_blocking: false, write_blocking: true)
@@handlers = {} of ::Signal => Handler
@@sigset = Sigset.new
class_setter child_handler : Handler?
@@mutex = Mutex.new(:unchecked)

def self.trap(signal, handler) : Nil
@@mutex.synchronize do
unless @@handlers[signal]?
@@sigset << signal
LibC.signal(signal.value, ->(value : Int32) {
writer.write_bytes(value) unless writer.closed?
})
end
@@handlers[signal] = handler
end
end

def self.reset(signal) : Nil
set(signal, LibC::SIG_DFL)
end

def self.ignore(signal) : Nil
set(signal, LibC::SIG_IGN)
end

private def self.set(signal, handler)
if signal == ::Signal::CHLD
# Clear any existing signal child handler
@@child_handler = nil
# But keep a default SIGCHLD, Process#wait requires it
trap(signal, ->(signal : ::Signal) {
SignalChildHandler.call
@@child_handler.try(&.call(signal))
})
else
@@mutex.synchronize do
@@handlers.delete(signal)
LibC.signal(signal, handler)
@@sigset.delete(signal)
end
end
end

private def self.start_loop
spawn(name: "Signal Loop") do
loop do
value = reader.read_bytes(Int32)
rescue IO::Error
next
else
process(::Signal.new(value))
end
end
end

private def self.process(signal) : Nil
if handler = @@handlers[signal]?
non_nil_handler = handler # if handler is closured it will also have the Nil type
spawn do
non_nil_handler.call(signal)
rescue ex
ex.inspect_with_backtrace(STDERR)
fatal("uncaught exception while processing handler for #{signal}")
end
else
fatal("missing handler for #{signal}")
end
end

# Replaces the signal pipe so the child process won't share the file
# descriptors of the parent process and send it received signals.
def self.after_fork
@@pipe.each(&.file_descriptor_close)
ensure
@@pipe = IO.pipe(read_blocking: false, write_blocking: true)
end

# Resets signal handlers to `SIG_DFL`. This avoids the child to receive
# signals that would be sent to the parent process through the signal
# pipe.
#
# We keep a signal set to because accessing @@handlers isn't thread safe —a
# thread could be mutating the hash while another one forked. This allows to
# only reset a few signals (fast) rather than all (very slow).
#
# We eventually close the pipe anyway to avoid a potential race where a sigset
# wouldn't exactly reflect actual signal state. This avoids sending a children
# signal to the parent. Exec will reset the signals properly for the
# sub-process.
def self.after_fork_before_exec
::Signal.each do |signal|
LibC.signal(signal, LibC::SIG_DFL) if @@sigset.includes?(signal)
end
ensure
{% unless flag?(:preview_mt) %}
@@pipe.each(&.file_descriptor_close)
{% end %}
end

private def self.reader
@@pipe[0]
end

private def self.writer
@@pipe[1]
end

private def self.fatal(message : String)
STDERR.puts("FATAL: #{message}, exiting")
STDERR.flush
LibC._exit(1)
end

@@setup_default_handlers = Atomic::Flag.new
@@setup_segfault_handler = Atomic::Flag.new
@@segfault_handler = LibC::SigactionHandlerT.new { |sig, info, data|
# Capture fault signals (SEGV, BUS) and finish the process printing a backtrace first

# Determine if the SEGV was inside or 'near' the top of the stack
# to check for potential stack overflow. 'Near' is a small
# amount larger than a typical stack frame, 4096 bytes here.
addr = info.value.si_addr

is_stack_overflow =
begin
stack_top = Pointer(Void).new(::Fiber.current.@stack.address - 4096)
stack_bottom = ::Fiber.current.@stack_bottom
stack_top <= addr < stack_bottom
rescue e
Crystal::System.print_error "Error while trying to determine if a stack overflow has occurred. Probable memory corruption\n"
false
end

if is_stack_overflow
Crystal::System.print_error "Stack overflow (e.g., infinite or very deep recursion)\n"
else
Crystal::System.print_error "Invalid memory access (signal %d) at address 0x%lx\n", sig, addr
end

Exception::CallStack.print_backtrace
LibC._exit(sig)
}

def self.setup_default_handlers : Nil
return unless @@setup_default_handlers.test_and_set
@@sigset.clear
start_loop
::Signal::PIPE.ignore
::Signal::CHLD.reset
end

def self.setup_segfault_handler
return unless @@setup_segfault_handler.test_and_set

altstack = LibC::StackT.new
altstack.ss_sp = LibC.malloc(LibC::SIGSTKSZ)
altstack.ss_size = LibC::SIGSTKSZ
altstack.ss_flags = 0
LibC.sigaltstack(pointerof(altstack), nil)

action = LibC::Sigaction.new
action.sa_flags = LibC::SA_ONSTACK | LibC::SA_SIGINFO
action.sa_sigaction = @@segfault_handler
LibC.sigemptyset(pointerof(action.@sa_mask))

LibC.sigaction(::Signal::SEGV, pointerof(action), nil)
LibC.sigaction(::Signal::BUS, pointerof(action), nil)
end
end

struct Crystal::System::Sigset
{% if flag?(:darwin) || flag?(:openbsd) %}
@set = LibC::SigsetT.new(0)
{% else %}
@set = LibC::SigsetT.new
{% end %}

def to_unsafe
pointerof(@set)
end

def <<(signal) : Nil
LibC.sigaddset(pointerof(@set), signal)
end

def delete(signal) : Nil
LibC.sigdelset(pointerof(@set), signal)
end

def includes?(signal) : Bool
LibC.sigismember(pointerof(@set), signal) == 1
end

def clear : Nil
LibC.sigemptyset(pointerof(@set))
end
end

module Crystal::System::SignalChildHandler
# Process#wait will block until the sub-process has terminated. On POSIX
# systems, the SIGCHLD signal is triggered. We thus always trap SIGCHLD then
# reap/memorize terminated child processes and eventually notify
# Process#wait through a channel, that may be created before or after the
# child process exited.

@@pending = {} of LibC::PidT => Int32
@@waiting = {} of LibC::PidT => Channel(Int32)
@@mutex = Mutex.new(:unchecked)

def self.wait(pid : LibC::PidT) : Channel(Int32)
channel = Channel(Int32).new(1)

@@mutex.lock
if exit_code = @@pending.delete(pid)
@@mutex.unlock
channel.send(exit_code)
channel.close
else
@@waiting[pid] = channel
@@mutex.unlock
end

channel
end

def self.call : Nil
loop do
pid = LibC.waitpid(-1, out exit_code, LibC::WNOHANG)

case pid
when 0
return
when -1
return if Errno.value == Errno::ECHILD
raise RuntimeError.from_errno("waitpid")
else
@@mutex.lock
if channel = @@waiting.delete(pid)
@@mutex.unlock
channel.send(exit_code)
channel.close
else
@@pending[pid] = exit_code
@@mutex.unlock
end
end
end
end

def self.after_fork
@@pending.clear
@@waiting.each_value(&.close)
@@waiting.clear
end
end
Loading