From 5a0e21fe023c5a4e76edf176c11ccb518cfc15d5 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Wed, 21 Feb 2018 18:05:15 +0100 Subject: [PATCH] Refactor signal handlers (#5730) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking change: - Harness the SIGCHLD handling, which is required by Process#wait. Now we always handle SIGCHLD using SignalChildHandler. Trying to reset or ignore SIGCHLD will actually set the default handler, trying to trap SIGCHLD will wrap the custom handler instead. Fixes: - Synchronize some accesses using a Mutex and an Atomic to further enhance potential concurrency issues —probably impossible until parallelism is implemented. - No longer closes the file descriptor at exit, which prevents an unhandled exception when receiving a signal while the program is exiting. - Restore STDIN/OUT/ERR blocking state on exit. Simplify implementation: - Move private types (SignalHandler, SignalChildHandler) to the private Crystal namespace. - Rename SignalHandler to Crystal::Signal. - No more singleton classes. - Using a Channel directly instead of a Concurrent::Future. - Using macros under enum (it wasn't possible before). - Introduce LibC::SIG_DFL and LibC::SIG_IGN definitions. --- spec/std/signal_spec.cr | 26 ++ src/compiler/crystal/command.cr | 8 +- src/event/signal_child_handler.cr | 68 ---- src/event/signal_handler.cr | 95 ------ src/io.cr | 2 +- src/kernel.cr | 11 +- src/lib_c/aarch64-linux-gnu/c/signal.cr | 4 + src/lib_c/amd64-unknown-openbsd/c/signal.cr | 4 + src/lib_c/arm-linux-gnueabihf/c/signal.cr | 4 + src/lib_c/i686-linux-gnu/c/signal.cr | 4 + src/lib_c/i686-linux-musl/c/signal.cr | 4 + src/lib_c/x86_64-linux-gnu/c/signal.cr | 4 + src/lib_c/x86_64-linux-musl/c/signal.cr | 4 + src/lib_c/x86_64-macosx-darwin/c/signal.cr | 4 + src/lib_c/x86_64-portbld-freebsd/c/signal.cr | 4 + src/process.cr | 11 +- src/signal.cr | 332 +++++++++++++------ 17 files changed, 304 insertions(+), 285 deletions(-) delete mode 100644 src/event/signal_child_handler.cr delete mode 100644 src/event/signal_handler.cr diff --git a/spec/std/signal_spec.cr b/spec/std/signal_spec.cr index ce00a18933de..c63f177c3d37 100644 --- a/spec/std/signal_spec.cr +++ b/spec/std/signal_spec.cr @@ -24,5 +24,31 @@ describe "Signal" do Process.kill Signal::USR2, Process.pid end + it "CHLD.reset sets default Crystal child handler" do + Signal::CHLD.reset + child = Process.new("true", shell: true) + child.wait # doesn't block forever + end + + it "CHLD.ignore sets default Crystal child handler" do + Signal::CHLD.ignore + child = Process.new("true", shell: true) + child.wait # doesn't block forever + end + + it "CHLD.trap is called after default Crystal child handler" do + called = false + child = nil + + Signal::CHLD.trap do + called = true + Process.exists?(child.not_nil!.pid).should be_false + end + + child = Process.new("true", shell: true) + child.not_nil!.wait # doesn't block forever + called.should be_true + end + # TODO: test Signal::X.reset end diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index 6a789f0ce991..ac1b64c74b7a 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -211,7 +211,7 @@ class Crystal::Command Process.run(output_filename, args: run_args, input: Process::Redirect::Inherit, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) do |process| # Ignore the signal so we don't exit the running process # (the running process can still handle this signal) - Signal::INT.ignore # do + ::Signal::INT.ignore # do end end {$?, elapsed} @@ -235,11 +235,11 @@ class Crystal::Command exit status.exit_code else case status.exit_signal - when Signal::KILL + when ::Signal::KILL STDERR.puts "Program was killed" - when Signal::SEGV + when ::Signal::SEGV STDERR.puts "Program exited because of a segmentation fault (11)" - when Signal::INT + when ::Signal::INT # OK, bubbled from the sub-program else STDERR.puts "Program received and didn't handle signal #{status.exit_signal} (#{status.exit_signal.value})" diff --git a/src/event/signal_child_handler.cr b/src/event/signal_child_handler.cr deleted file mode 100644 index 694a03386879..000000000000 --- a/src/event/signal_child_handler.cr +++ /dev/null @@ -1,68 +0,0 @@ -require "c/sys/wait" - -# :nodoc: -# Singleton that handles `SIG_CHLD` and queues events for `Process#waitpid`. -# `Process.waitpid` uses this class for nonblocking operation. -class Event::SignalChildHandler - def self.instance : self - @@instance ||= begin - Signal.setup_default_handlers - new - end - end - - alias ChanType = Channel::Buffered(Process::Status?) - - def initialize - @pending = Hash(LibC::PidT, Process::Status).new - @waiting = Hash(LibC::PidT, ChanType).new - end - - def after_fork - @pending.clear - @waiting.each { |pid, chan| chan.send(nil) } - @waiting.clear - end - - def trigger - loop do - pid = LibC.waitpid(-1, out exit_code, LibC::WNOHANG) - case pid - when 0 - return nil - when -1 - raise Errno.new("waitpid") unless Errno.value == Errno::ECHILD - return nil - else - status = Process::Status.new exit_code - send_pending pid, status - end - end - end - - private def send_pending(pid, status) - # BUG: needs mutexes with threads - if chan = @waiting[pid]? - chan.send status - @waiting.delete pid - else - @pending[pid] = status - end - end - - # Returns a future that sends a `Process::Status` or raises after forking. - def waitpid(pid : LibC::PidT) - chan = ChanType.new(1) - # BUG: needs mutexes with threads - if status = @pending[pid]? - chan.send status - @pending.delete pid - else - @waiting[pid] = chan - end - - lazy do - chan.receive || raise Channel::ClosedError.new("waitpid channel closed after forking") - end - end -end diff --git a/src/event/signal_handler.cr b/src/event/signal_handler.cr deleted file mode 100644 index 857a2bea9701..000000000000 --- a/src/event/signal_handler.cr +++ /dev/null @@ -1,95 +0,0 @@ -require "c/signal" -require "c/unistd" - -# :nodoc: -# Singleton that runs Signal events (libevent2) in it's own Fiber. -class Event::SignalHandler - def self.add_handler(*args) - instance.add_handler *args - end - - def self.del_handler(signal) - @@instance.try &.del_handler(signal) - end - - def self.after_fork - @@instance.try &.after_fork - end - - # finish processing signals - def self.close - @@instance.try &.close - @@instance = nil - end - - private def self.instance - @@instance ||= new - end - - @read_pipe : IO::FileDescriptor - @write_pipe : IO::FileDescriptor - - @@write_pipe : IO::FileDescriptor? - - def initialize - @callbacks = Hash(Signal, (Signal ->)).new - @read_pipe, @write_pipe = IO.pipe - @@write_pipe = @write_pipe - - spawn_reader - end - - # :nodoc: - def run - read_pipe = @read_pipe - - loop do - sig = read_pipe.read_bytes(Int32) - handle_signal Signal.new(sig) - end - end - - def after_fork - close - @read_pipe, @write_pipe = IO.pipe - @@write_pipe = @write_pipe - spawn_reader - end - - def close - # Close writer only: reader will give EOF - @write_pipe.close - end - - def add_handler(signal : Signal, callback) - @callbacks[signal] = callback - - LibC.signal signal.value, ->(sig : Int32) do - @@write_pipe.not_nil!.write_bytes sig - nil - end - end - - def del_handler(signal : Signal) - if callback = @callbacks[signal]? - @callbacks.delete signal - end - end - - private def handle_signal(sig) - if callback = @callbacks[sig]? - callback.call sig - else - raise "Missing #{sig} callback" - end - rescue ex - ex.inspect_with_backtrace STDERR - STDERR.puts "FATAL ERROR: uncaught signal #{sig} exception, exiting" - STDERR.flush - LibC._exit 1 - end - - private def spawn_reader - spawn { run } - end -end diff --git a/src/io.cr b/src/io.cr index d528fe2f84ec..9e56ec5afbb7 100644 --- a/src/io.cr +++ b/src/io.cr @@ -145,7 +145,7 @@ abstract class IO # reader.gets # => "hello" # reader.gets # => "world" # ``` - def self.pipe(read_blocking = false, write_blocking = false) + def self.pipe(read_blocking = false, write_blocking = false) : {IO::FileDescriptor, IO::FileDescriptor} pipe_fds = uninitialized StaticArray(LibC::Int, 2) if LibC.pipe(pipe_fds) != 0 raise Errno.new("Could not create pipe") diff --git a/src/kernel.cr b/src/kernel.cr index 89031da4d4df..7c154550c76f 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -189,18 +189,14 @@ class Process def self.after_fork_child_callbacks @@after_fork_child_callbacks ||= [ ->Scheduler.after_fork, - ->Event::SignalHandler.after_fork, - ->{ Event::SignalChildHandler.instance.after_fork }, + ->Crystal::Signal.after_fork, + ->Crystal::SignalChildHandler.after_fork, ->Random::DEFAULT.new_seed, ] of -> Nil end end {% unless flag?(:win32) %} - Signal.setup_default_handlers - - at_exit { Event::SignalHandler.close } - # Background loop to cleanup unused fiber stacks. spawn do loop do @@ -208,4 +204,7 @@ end Fiber.stack_pool_collect end end + + Signal.setup_default_handlers + LibExt.setup_sigfault_handler {% end %} diff --git a/src/lib_c/aarch64-linux-gnu/c/signal.cr b/src/lib_c/aarch64-linux-gnu/c/signal.cr index 9bd7fed172c7..bb79c4cecd3a 100644 --- a/src/lib_c/aarch64-linux-gnu/c/signal.cr +++ b/src/lib_c/aarch64-linux-gnu/c/signal.cr @@ -36,6 +36,10 @@ lib LibC SIGSTKFLT = 16 SIGUNUSED = 31 + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(pid : PidT, sig : Int) : Int fun signal(sig : Int, handler : Int -> Void) : Int -> Void end diff --git a/src/lib_c/amd64-unknown-openbsd/c/signal.cr b/src/lib_c/amd64-unknown-openbsd/c/signal.cr index af28492a9d79..1be87179cf4b 100644 --- a/src/lib_c/amd64-unknown-openbsd/c/signal.cr +++ b/src/lib_c/amd64-unknown-openbsd/c/signal.cr @@ -34,6 +34,10 @@ lib LibC SIGINFO = 29 SIGWINCH = 28 + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(x0 : PidT, x1 : Int) : Int fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void end diff --git a/src/lib_c/arm-linux-gnueabihf/c/signal.cr b/src/lib_c/arm-linux-gnueabihf/c/signal.cr index 9bd7fed172c7..bb79c4cecd3a 100644 --- a/src/lib_c/arm-linux-gnueabihf/c/signal.cr +++ b/src/lib_c/arm-linux-gnueabihf/c/signal.cr @@ -36,6 +36,10 @@ lib LibC SIGSTKFLT = 16 SIGUNUSED = 31 + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(pid : PidT, sig : Int) : Int fun signal(sig : Int, handler : Int -> Void) : Int -> Void end diff --git a/src/lib_c/i686-linux-gnu/c/signal.cr b/src/lib_c/i686-linux-gnu/c/signal.cr index 9bd7fed172c7..bb79c4cecd3a 100644 --- a/src/lib_c/i686-linux-gnu/c/signal.cr +++ b/src/lib_c/i686-linux-gnu/c/signal.cr @@ -36,6 +36,10 @@ lib LibC SIGSTKFLT = 16 SIGUNUSED = 31 + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(pid : PidT, sig : Int) : Int fun signal(sig : Int, handler : Int -> Void) : Int -> Void end diff --git a/src/lib_c/i686-linux-musl/c/signal.cr b/src/lib_c/i686-linux-musl/c/signal.cr index 47511a01b73e..a1133223cb51 100644 --- a/src/lib_c/i686-linux-musl/c/signal.cr +++ b/src/lib_c/i686-linux-musl/c/signal.cr @@ -35,6 +35,10 @@ lib LibC SIGSTKFLT = 16 SIGUNUSED = LibC::SIGSYS + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(x0 : PidT, x1 : Int) : Int fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void end diff --git a/src/lib_c/x86_64-linux-gnu/c/signal.cr b/src/lib_c/x86_64-linux-gnu/c/signal.cr index 9bd7fed172c7..bb79c4cecd3a 100644 --- a/src/lib_c/x86_64-linux-gnu/c/signal.cr +++ b/src/lib_c/x86_64-linux-gnu/c/signal.cr @@ -36,6 +36,10 @@ lib LibC SIGSTKFLT = 16 SIGUNUSED = 31 + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(pid : PidT, sig : Int) : Int fun signal(sig : Int, handler : Int -> Void) : Int -> Void end diff --git a/src/lib_c/x86_64-linux-musl/c/signal.cr b/src/lib_c/x86_64-linux-musl/c/signal.cr index 47511a01b73e..a1133223cb51 100644 --- a/src/lib_c/x86_64-linux-musl/c/signal.cr +++ b/src/lib_c/x86_64-linux-musl/c/signal.cr @@ -35,6 +35,10 @@ lib LibC SIGSTKFLT = 16 SIGUNUSED = LibC::SIGSYS + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(x0 : PidT, x1 : Int) : Int fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void end diff --git a/src/lib_c/x86_64-macosx-darwin/c/signal.cr b/src/lib_c/x86_64-macosx-darwin/c/signal.cr index af28492a9d79..1be87179cf4b 100644 --- a/src/lib_c/x86_64-macosx-darwin/c/signal.cr +++ b/src/lib_c/x86_64-macosx-darwin/c/signal.cr @@ -34,6 +34,10 @@ lib LibC SIGINFO = 29 SIGWINCH = 28 + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(x0 : PidT, x1 : Int) : Int fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void end diff --git a/src/lib_c/x86_64-portbld-freebsd/c/signal.cr b/src/lib_c/x86_64-portbld-freebsd/c/signal.cr index af28492a9d79..1be87179cf4b 100644 --- a/src/lib_c/x86_64-portbld-freebsd/c/signal.cr +++ b/src/lib_c/x86_64-portbld-freebsd/c/signal.cr @@ -34,6 +34,10 @@ lib LibC SIGINFO = 29 SIGWINCH = 28 + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + fun kill(x0 : PidT, x1 : Int) : Int fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void end diff --git a/src/process.cr b/src/process.cr index 104d8b162521..ca6a373ae8c7 100644 --- a/src/process.cr +++ b/src/process.cr @@ -1,7 +1,6 @@ require "c/signal" require "c/stdlib" require "c/sys/times" -require "c/sys/wait" require "c/unistd" class Process @@ -198,7 +197,7 @@ class Process # A pipe to this process's error. Raises if a pipe wasn't asked when creating the process. getter! error : IO::FileDescriptor - @waitpid_future : Concurrent::Future(Process::Status) + @waitpid : Channel::Buffered(Int32) # Creates a process, executes it, but doesn't wait for it to complete. # @@ -260,7 +259,7 @@ class Process end end - @waitpid_future = Event::SignalChildHandler.instance.waitpid(pid) + @waitpid = Crystal::SignalChildHandler.wait(pid) fork_input.try &.close fork_output.try &.close @@ -268,7 +267,7 @@ class Process end private def initialize(@pid) - @waitpid_future = Event::SignalChildHandler.instance.waitpid(pid) + @waitpid = Crystal::SignalChildHandler.wait(pid) @wait_count = 0 end @@ -287,7 +286,7 @@ class Process end @wait_count = 0 - @waitpid_future.get + Process::Status.new(@waitpid.receive) ensure close end @@ -300,7 +299,7 @@ class Process # Whether this process is already terminated. def terminated? - @waitpid_future.completed? || !Process.exists?(@pid) + @waitpid.closed? || !Process.exists?(@pid) end # Closes any pipes to the child process. diff --git a/src/signal.cr b/src/signal.cr index 2089de36645e..186e6b13f003 100644 --- a/src/signal.cr +++ b/src/signal.cr @@ -1,78 +1,13 @@ require "c/signal" require "c/stdio" +require "c/sys/wait" require "c/unistd" -{% if flag?(:linux) %} - enum Signal - 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 - - PWR = LibC::SIGPWR - STKFLT = LibC::SIGSTKFLT - UNUSED = LibC::SIGUNUSED - end -{% else %} - enum Signal - 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 - end -{% end %} - -# This module provides a way to handle OS signals passed to the process. +# Safely handle inter-process signals on POSIX systems. +# +# Signals are dispatched to the event loop and later processed in a dedicated +# fiber. Some received signals may never be processed when the program +# terminates. # # ``` # puts "Ctrl+C still has the OS default action (stops the program)" @@ -90,56 +25,245 @@ require "c/unistd" # ``` # # Note: -# - Signals are processed through the event loop and run in their own Fiber. -# - Signals may be lost if the event loop doesn't run before exit. # - An uncaught exception in a signal handler is a fatal error. -enum Signal +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 + {% end %} + # Sets the handler for this signal to the passed function. # # After executing this, whenever the current process receives the - # corresponding signal, the passed function will be run (instead of the OS - # default). - def trap(block : Signal ->) - trap &block + # corresponding signal, the passed function will be called (instead of the OS + # default). The handler will run in a signal-safe fiber thought the event + # loop; there is no limit to what functions can be called, unlike raw signals + # that run on the sigaltstack. + # + # Note that `CHLD` is always trapped and child processes will always be reaped + # 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. + def trap(&handler : Signal ->) : Nil + if self == CHLD + Crystal::Signal.child_handler = handler + else + Crystal::Signal.trap(self, handler) + end end - # ditto - def trap(&block : Signal ->) - Event::SignalHandler.add_handler self, block + # Resets the handler for this signal to the OS default. + # + # Note that trying to reset `CHLD` will actually set the default crystal + # 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) end - # Resets the handler for this signal to the OS default. - def reset - case self - when CHLD - # don't ignore by default. send events to a waitpid service - trap do - Event::SignalChildHandler.instance.trigger + # Clears the handler for this signal and prevents the OS default action. + # + # Note that trying to ignore `CHLD` will actually set the default crystal + # 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 + + @@setup_default_handlers = Atomic(Int32).new(0) + + # :nodoc: + def self.setup_default_handlers + _, success = @@setup_default_handlers.compare_and_set(0, 1) + return unless success + + Crystal::Signal.start_loop + Signal::PIPE.ignore + Signal::CHLD.reset + 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 + + def self.trap(signal, handler) : Nil + @@mutex.synchronize do + unless @@handlers[signal]? + LibC.signal(signal.value, ->(value : Int32) { + writer.write_bytes(value) + }) + 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 + # don't reset/ignore 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.value, handler) + end + end + end + + def self.start_loop + spawn do + loop do + value = reader.read_bytes(Int32) + process(::Signal.new(value)) end + end + end + + private def self.process(signal) : Nil + if handler = @@handlers[signal]? + handler.call(signal) else - del_handler Proc(Int32, Void).new(Pointer(Void).new(0_u64), Pointer(Void).null) + fatal("missing handler for #{signal}") end + rescue ex + ex.inspect_with_backtrace(STDERR) + fatal("uncaught exception while processing handler for #{signal}") end - # Clears the handler for this signal and prevents the OS default action. - def ignore - del_handler Proc(Int32, Void).new(Pointer(Void).new(1_u64), Pointer(Void).null) + def self.after_fork + @@pipe = IO.pipe(read_blocking: false, write_blocking: true) end - private def del_handler(block) - Event::SignalHandler.del_handler self - LibC.signal value, block + private def self.reader + @@pipe[0] end - @@default_handlers_setup = false + private def self.writer + @@pipe[1] + end - # :nodoc: - def self.setup_default_handlers - return if @@default_handlers_setup - @@default_handlers_setup = true + private def self.fatal(message : String) + Crystal.restore_blocking_state - Signal::PIPE.ignore - Signal::CHLD.reset + 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::Buffered(Int32) + @@mutex = Mutex.new + + def self.wait(pid : LibC::PidT) : Channel::Buffered(Int32) + channel = Channel::Buffered(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 Errno.new("waitpid") + end + + @@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 + + def self.after_fork + @@pending.clear + @@waiting.each_value(&.close) + @@waiting.clear end end @@ -150,7 +274,5 @@ fun __crystal_sigfault_handler(sig : LibC::Int, addr : Void*) # Capture fault signals (SEGV, BUS) and finish the process printing a backtrace first LibC.dprintf 2, "Invalid memory access (signal %d) at address 0x%lx\n", sig, addr CallStack.print_backtrace - LibC._exit sig + LibC._exit(sig) end - -LibExt.setup_sigfault_handler