diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 349281ec16e8..1088c0130b35 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -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 @@ -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 @@ -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) diff --git a/src/crystal/system/file.cr b/src/crystal/system/file.cr index b5202585d377..0f645fd8a3ec 100644 --- a/src/crystal/system/file.cr +++ b/src/crystal/system/file.cr @@ -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 diff --git a/src/crystal/system/signal.cr b/src/crystal/system/signal.cr new file mode 100644 index 000000000000..b5ba591b2ec5 --- /dev/null +++ b/src/crystal/system/signal.cr @@ -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 %} diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index 407ebebc6151..ce536b354e50 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -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 @@ -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) @@ -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 diff --git a/src/crystal/system/unix/signal.cr b/src/crystal/system/unix/signal.cr new file mode 100644 index 000000000000..c30a2b985af2 --- /dev/null +++ b/src/crystal/system/unix/signal.cr @@ -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 diff --git a/src/crystal/system/wasi/signal.cr b/src/crystal/system/wasi/signal.cr new file mode 100644 index 000000000000..d66b9c22c5cd --- /dev/null +++ b/src/crystal/system/wasi/signal.cr @@ -0,0 +1,13 @@ +module Crystal::System::Signal + def self.trap(signal, handler) : Nil + raise NotImplementedError.new("Crystal::System::Signal.trap") + end + + def self.reset(signal) : Nil + raise NotImplementedError.new("Crystal::System::Signal.reset") + end + + def self.ignore(signal) : Nil + raise NotImplementedError.new("Crystal::System::Signal.ignore") + end +end diff --git a/src/crystal/system/win32/signal.cr b/src/crystal/system/win32/signal.cr new file mode 100644 index 000000000000..8f5541c7599b --- /dev/null +++ b/src/crystal/system/win32/signal.cr @@ -0,0 +1,15 @@ +require "c/signal" + +module Crystal::System::Signal + def self.trap(signal, handler) : Nil + raise NotImplementedError.new("Crystal::System::Signal.trap") + end + + def self.reset(signal) : Nil + raise NotImplementedError.new("Crystal::System::Signal.reset") + end + + def self.ignore(signal) : Nil + raise NotImplementedError.new("Crystal::System::Signal.ignore") + end +end diff --git a/src/docs_main.cr b/src/docs_main.cr index 0a4f3962b864..661163677b7c 100644 --- a/src/docs_main.cr +++ b/src/docs_main.cr @@ -47,9 +47,7 @@ require "./option_parser" require "./path" require "./random/**" require "./semantic_version" -{% unless flag?(:win32) %} - require "./signal" -{% end %} +require "./signal" require "./string_pool" require "./string_scanner" require "./unicode/unicode" diff --git a/src/exception/call_stack/libunwind.cr b/src/exception/call_stack/libunwind.cr index ecb400bee0f7..21683c0351ef 100644 --- a/src/exception/call_stack/libunwind.cr +++ b/src/exception/call_stack/libunwind.cr @@ -33,7 +33,7 @@ struct Exception::CallStack {% end %} def self.setup_crash_handler - Signal.setup_segfault_handler + Crystal::System::Signal.setup_segfault_handler end {% if flag?(:interpreted) %} @[Primitive(:interpreter_call_stack_unwind)] {% end %} diff --git a/src/kernel.cr b/src/kernel.cr index 45df75ef1dbe..1afd480ae959 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -536,8 +536,8 @@ end def self.after_fork_child_callbacks @@after_fork_child_callbacks ||= [ # clean ups (don't depend on event loop): - ->Crystal::Signal.after_fork, - ->Crystal::SignalChildHandler.after_fork, + ->Crystal::System::Signal.after_fork, + ->Crystal::System::SignalChildHandler.after_fork, # reinit event loop: ->{ Crystal::Scheduler.event_loop.after_fork }, @@ -561,7 +561,7 @@ end {% if flag?(:win32) %} Crystal::System::Process.start_interrupt_loop {% else %} - Signal.setup_default_handlers + Crystal::System::Signal.setup_default_handlers {% end %} # load debug info on start up of the program is executed with CRYSTAL_LOAD_DEBUG_INFO=1 diff --git a/src/lib_c/x86_64-windows-msvc/c/signal.cr b/src/lib_c/x86_64-windows-msvc/c/signal.cr new file mode 100644 index 000000000000..fbe90cd8eca3 --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/signal.cr @@ -0,0 +1,9 @@ +lib LibC + SIGINT = 2 + SIGILL = 4 + SIGFPE = 8 + SIGSEGV = 11 + SIGTERM = 15 + SIGBREAK = 21 + SIGABRT = 22 +end diff --git a/src/prelude.cr b/src/prelude.cr index b6f98b2dd25c..f06f5bc87015 100644 --- a/src/prelude.cr +++ b/src/prelude.cr @@ -13,9 +13,6 @@ require "lib_c" require "macros" require "object" require "comparable" -{% if flag?(:win32) %} - require "windows_stubs" -{% end %} require "exception" require "iterable" require "iterator" @@ -70,7 +67,7 @@ require "range" require "reference" require "regex" require "set" -{% unless flag?(:win32) || flag?(:wasm32) %} +{% unless flag?(:wasm32) %} require "signal" {% end %} require "slice" diff --git a/src/process.cr b/src/process.cr index 69d3953e6a7a..358511aebca5 100644 --- a/src/process.cr +++ b/src/process.cr @@ -305,6 +305,10 @@ class Process end # Sends *signal* to this process. + # + # NOTE: `#terminate` is preferred over `signal(Signal::TERM)` and + # `signal(Signal::KILL)` as a portable alternative which also works on + # Windows. def signal(signal : Signal) : Nil Crystal::System::Process.signal(@process_info.pid, signal) end diff --git a/src/process/status.cr b/src/process/status.cr index 06b6ff103ae1..c7b78b1a4583 100644 --- a/src/process/status.cr +++ b/src/process/status.cr @@ -178,6 +178,9 @@ class Process::Status # received and didn't handle. Will raise if `signal_exit?` is `false`. # # Available only on Unix-like operating systems. + # + # NOTE: `#exit_reason` is preferred over this method as a portable alternative + # which also works on Windows. def exit_signal : Signal {% if flag?(:unix) && !flag?(:wasm32) %} Signal.from_value(signal_code) diff --git a/src/signal.cr b/src/signal.cr index 9335619d043f..60eba5b8e7f3 100644 --- a/src/signal.cr +++ b/src/signal.cr @@ -1,7 +1,4 @@ -require "c/signal" -require "c/stdio" -require "c/sys/wait" -require "c/unistd" +require "crystal/system/signal" # Safely handle inter-process signals on POSIX systems. # @@ -24,45 +21,75 @@ require "c/unistd" # sleep 3 # ``` # -# NOTE: `Process.on_interrupt` is preferred over `Signal::INT.trap`, as the -# former also works on Windows. -# # WARNING: An uncaught exception in a signal handler is a fatal error. +# +# ## Portability +# +# The set of available signals is platform-dependent. Only signals that exist on +# the target platform are available as members of this enum. +# +# * `ABRT`, `FPE`, `ILL`, `INT`, `SEGV`, and `TERM` are guaranteed to exist +# on all platforms. +# * `PWR`, `STKFLT`, and `UNUSED` only exist on Linux. +# * `BREAK` only exists on Windows. +# * All other signals exist on all POSIX platforms. +# +# The methods `#trap`, `#reset`, and `#ignore` may not be implemented at all on +# non-POSIX systems. +# +# The standard library provides several platform-agnostic APIs to achieve tasks +# that are typically solved with signals on POSIX systems: +# +# * The portable API for responding to an interrupt signal (`INT.trap`) is +# `Process.on_interrupt`. +# * The portable API for sending a `TERM` or `KILL` signal to a process is +# `Process#terminate`. +# * The portable API for retrieving the exit signal of a process +# (`Process::Status#exit_signal`) is `Process::Status#exit_reason`. enum Signal : Int32 - HUP = LibC::SIGHUP - INT = LibC::SIGINT - QUIT = LibC::SIGQUIT - ILL = LibC::SIGILL - TRAP = LibC::SIGTRAP - IOT = LibC::SIGIOT - ABRT = LibC::SIGABRT - FPE = LibC::SIGFPE - KILL = LibC::SIGKILL - BUS = LibC::SIGBUS - SEGV = LibC::SIGSEGV - SYS = LibC::SIGSYS - PIPE = LibC::SIGPIPE - ALRM = LibC::SIGALRM - TERM = LibC::SIGTERM - URG = LibC::SIGURG - STOP = LibC::SIGSTOP - TSTP = LibC::SIGTSTP - CONT = LibC::SIGCONT - CHLD = LibC::SIGCHLD - TTIN = LibC::SIGTTIN - TTOU = LibC::SIGTTOU - IO = LibC::SIGIO - XCPU = LibC::SIGXCPU - XFSZ = LibC::SIGXFSZ - VTALRM = LibC::SIGVTALRM - USR1 = LibC::SIGUSR1 - USR2 = LibC::SIGUSR2 - WINCH = LibC::SIGWINCH - - {% if flag?(:linux) %} - PWR = LibC::SIGPWR - STKFLT = LibC::SIGSTKFLT - UNUSED = LibC::SIGUNUSED + # Signals required by the ISO C standard. Since every supported platform must + # bind against a C runtime library, these constants must be defined at all + # times, even when the platform does not support POSIX signals + + INT = LibC::SIGINT + ILL = LibC::SIGILL + FPE = LibC::SIGFPE + SEGV = LibC::SIGSEGV + TERM = LibC::SIGTERM + ABRT = LibC::SIGABRT + + {% if flag?(:win32) %} + BREAK = LibC::SIGBREAK + {% else %} + HUP = LibC::SIGHUP + QUIT = LibC::SIGQUIT + TRAP = LibC::SIGTRAP + IOT = LibC::SIGIOT + KILL = LibC::SIGKILL + BUS = LibC::SIGBUS + SYS = LibC::SIGSYS + PIPE = LibC::SIGPIPE + ALRM = LibC::SIGALRM + URG = LibC::SIGURG + STOP = LibC::SIGSTOP + TSTP = LibC::SIGTSTP + CONT = LibC::SIGCONT + CHLD = LibC::SIGCHLD + TTIN = LibC::SIGTTIN + TTOU = LibC::SIGTTOU + IO = LibC::SIGIO + XCPU = LibC::SIGXCPU + XFSZ = LibC::SIGXFSZ + VTALRM = LibC::SIGVTALRM + USR1 = LibC::SIGUSR1 + USR2 = LibC::SIGUSR2 + WINCH = LibC::SIGWINCH + + {% if flag?(:linux) %} + PWR = LibC::SIGPWR + STKFLT = LibC::SIGSTKFLT + UNUSED = LibC::SIGUNUSED + {% end %} {% end %} # Sets the handler for this signal to the passed function. @@ -77,12 +104,17 @@ enum Signal : Int32 # before the custom handler is called, hence a custom `CHLD` handler must # check child processes using `Process.exists?`. Trying to use waitpid with a # zero or negative value won't work. + # + # NOTE: `Process.on_interrupt` is preferred over `Signal::INT.trap` as a + # portable alternative which also works on Windows. def trap(&handler : Signal ->) : Nil - if self == CHLD - Crystal::Signal.child_handler = handler - else - Crystal::Signal.trap(self, handler) - end + {% if @type.has_constant?("CHLD") %} + if self == CHLD + Crystal::System::Signal.child_handler = handler + return + end + {% end %} + Crystal::System::Signal.trap(self, handler) end # Resets the handler for this signal to the OS default. @@ -91,7 +123,7 @@ enum Signal : Int32 # handler that monitors and reaps child processes. This prevents zombie # processes and is required by `Process#wait` for example. def reset : Nil - Crystal::Signal.reset(self) + Crystal::System::Signal.reset(self) end # Clears the handler for this signal and prevents the OS default action. @@ -100,272 +132,6 @@ enum Signal : Int32 # handler that monitors and reaps child processes. This prevents zombie # processes and is required by `Process#wait` for example. def ignore : Nil - Crystal::Signal.ignore(self) - end - - {% if flag?(:darwin) || flag?(:openbsd) %} - @@sigset = LibC::SigsetT.new(0) - {% else %} - @@sigset = LibC::SigsetT.new - {% end %} - - # :nodoc: - def set_add : Nil - LibC.sigaddset(pointerof(@@sigset), self) - end - - # :nodoc: - def set_del : Nil - LibC.sigdelset(pointerof(@@sigset), self) - end - - # :nodoc: - def set? : Bool - LibC.sigismember(pointerof(@@sigset), self) == 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) - } - - # :nodoc: - def self.setup_default_handlers : Nil - return unless @@setup_default_handlers.test_and_set - LibC.sigemptyset(pointerof(@@sigset)) - Crystal::Signal.start_loop - Signal::PIPE.ignore - Signal::CHLD.reset - end - - # :nodoc: - 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(SEGV, pointerof(action), nil) - LibC.sigaction(BUS, pointerof(action), nil) - end -end - -# :nodoc: -module Crystal::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 - @@child_handler : Handler? - @@mutex = Mutex.new(:unchecked) - - def self.trap(signal, handler) : Nil - @@mutex.synchronize do - unless @@handlers[signal]? - signal.set_add - LibC.signal(signal.value, ->(value : Int32) { - writer.write_bytes(value) unless writer.closed? - }) - end - @@handlers[signal] = handler - end - end - - def self.child_handler=(handler : Handler) : Nil - @@child_handler = handler - 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) { - Crystal::SignalChildHandler.call - @@child_handler.try(&.call(signal)) - }) - else - @@mutex.synchronize do - @@handlers.delete(signal) - LibC.signal(signal, handler) - signal.set_del - end - end - end - - 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 signal.set? - 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 -end - -# :nodoc: -module Crystal::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 + Crystal::System::Signal.ignore(self) end end diff --git a/src/windows_stubs.cr b/src/windows_stubs.cr deleted file mode 100644 index 17aba1c25289..000000000000 --- a/src/windows_stubs.cr +++ /dev/null @@ -1,3 +0,0 @@ -enum Signal - KILL = 0 -end