Skip to content

Commit

Permalink
Add full stub for Windows signals (#13131)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Mar 8, 2023
1 parent dd4b89f commit bbe28a0
Show file tree
Hide file tree
Showing 16 changed files with 445 additions and 351 deletions.
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

0 comments on commit bbe28a0

Please sign in to comment.