diff --git a/spec/std/process/status_spec.cr b/spec/std/process/status_spec.cr index 45f60aba4c06..470a0a1a34d9 100644 --- a/spec/std/process/status_spec.cr +++ b/spec/std/process/status_spec.cr @@ -149,10 +149,16 @@ describe Process::Status do it "returns Aborted" do Process::Status.new(Signal::ABRT.value).exit_reason.aborted?.should be_true - Process::Status.new(Signal::HUP.value).exit_reason.aborted?.should be_true Process::Status.new(Signal::KILL.value).exit_reason.aborted?.should be_true Process::Status.new(Signal::QUIT.value).exit_reason.aborted?.should be_true - Process::Status.new(Signal::TERM.value).exit_reason.aborted?.should be_true + end + + it "returns TerminalDisconnected" do + Process::Status.new(Signal::HUP.value).exit_reason.terminal_disconnected?.should be_true + end + + it "returns SessionEnded" do + Process::Status.new(Signal::TERM.value).exit_reason.session_ended?.should be_true end it "returns Interrupted" do diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 9734ec5ea99c..f303cdb5862f 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -348,6 +348,14 @@ describe Process do end end + describe ".on_terminate" do + it "compiles" do + typeof(Process.on_terminate { }) + typeof(Process.ignore_interrupts!) + typeof(Process.restore_interrupts!) + end + end + {% unless flag?(:win32) %} describe "#signal(Signal::KILL)" do it "kills a process" do diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index b51f9405c4e6..431c9114a6b7 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -312,7 +312,7 @@ class Crystal::Command private def exit_message(status) case status.exit_reason - when .aborted? + when .aborted?, .session_ended?, .terminal_disconnected? if status.signal_exit? signal = status.exit_signal if signal.kill? diff --git a/src/crystal/system/process.cr b/src/crystal/system/process.cr index 387447c083c2..fcc08adbbec3 100644 --- a/src/crystal/system/process.cr +++ b/src/crystal/system/process.cr @@ -46,6 +46,10 @@ struct Crystal::System::Process # previously set interrupt handler. # def self.on_interrupt(&handler : ->) + # Installs *handler* as the new handler for termination signals. Removes any + # previously set handler. + # def self.on_terminate(&handler : ::Process::ExitReason ->) + # Ignores all interrupt requests. Removes any custom interrupt handler set # def self.ignore_interrupts! diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index 762507a590d4..4fd2b658cd59 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -58,10 +58,35 @@ struct Crystal::System::Process raise RuntimeError.from_errno("kill") if ret < 0 end + @[Deprecated("Use `#on_terminate` instead")] def self.on_interrupt(&handler : ->) : Nil ::Signal::INT.trap { |_signal| handler.call } end + def self.on_terminate(&handler : ::Process::ExitReason ->) : Nil + sig_handler = Proc(::Signal, Nil).new do |signal| + int_type = case signal + when .int? + ::Process::ExitReason::Interrupted + when .hup? + ::Process::ExitReason::TerminalDisconnected + when .term? + ::Process::ExitReason::SessionEnded + else + ::Process::ExitReason::Interrupted + end + handler.call int_type + + # ignore prevents system defaults and clears registered interrupts + # hence we need to re-register + signal.ignore + Process.on_terminate &handler + end + ::Signal::INT.trap &sig_handler + ::Signal::HUP.trap &sig_handler + ::Signal::TERM.trap &sig_handler + end + def self.ignore_interrupts! : Nil ::Signal::INT.ignore end diff --git a/src/crystal/system/wasi/process.cr b/src/crystal/system/wasi/process.cr index a6f9d156c396..cae182f1ac50 100644 --- a/src/crystal/system/wasi/process.cr +++ b/src/crystal/system/wasi/process.cr @@ -48,10 +48,15 @@ struct Crystal::System::Process raise NotImplementedError.new("Process.signal") end + @[Deprecated("Use `#on_terminate` instead")] def self.on_interrupt(&handler : ->) : Nil raise NotImplementedError.new("Process.on_interrupt") end + def self.on_terminate(&handler : ::Process::ExitReason ->) : Nil + raise NotImplementedError.new("Process.on_terminate") + end + def self.ignore_interrupts! : Nil raise NotImplementedError.new("Process.ignore_interrupts!") end diff --git a/src/crystal/system/win32/process.cr b/src/crystal/system/win32/process.cr index b92203b38510..a8a89f442fab 100644 --- a/src/crystal/system/win32/process.cr +++ b/src/crystal/system/win32/process.cr @@ -13,10 +13,11 @@ struct Crystal::System::Process @job_object : LibC::HANDLE @completion_key = IO::Overlapped::CompletionKey.new - @@interrupt_handler : Proc(Nil)? + @@interrupt_handler : Proc(::Process::ExitReason, Nil)? @@interrupt_count = Crystal::AtomicSemaphore.new @@win32_interrupt_handler : LibC::PHANDLER_ROUTINE? @@setup_interrupt_handler = Atomic::Flag.new + @@last_interrupt = ::Process::ExitReason::Interrupted def initialize(process_info) @pid = process_info.dwProcessId @@ -150,10 +151,26 @@ struct Crystal::System::Process raise NotImplementedError.new("Process.signal") end - def self.on_interrupt(&@@interrupt_handler : ->) : Nil + @[Deprecated("Use `#on_terminate` instead")] + def self.on_interrupt(&handler : ->) : Nil + on_terminate do |reason| + handler.call if reason.interrupted? + end + end + + def self.on_terminate(&@@interrupt_handler : ::Process::ExitReason ->) : Nil restore_interrupts! @@win32_interrupt_handler = handler = LibC::PHANDLER_ROUTINE.new do |event_type| - next 0 unless event_type.in?(LibC::CTRL_C_EVENT, LibC::CTRL_BREAK_EVENT) + @@last_interrupt = case event_type + when LibC::CTRL_C_EVENT, LibC::CTRL_BREAK_EVENT + ::Process::ExitReason::Interrupted + when LibC::CTRL_CLOSE_EVENT + ::Process::ExitReason::TerminalDisconnected + when LibC::CTRL_LOGOFF_EVENT, LibC::CTRL_SHUTDOWN_EVENT + ::Process::ExitReason::SessionEnded + else + next 0 + end @@interrupt_count.signal 1 end @@ -186,8 +203,9 @@ struct Crystal::System::Process if handler = @@interrupt_handler non_nil_handler = handler # if handler is closured it will also have the Nil type + int_type = @@last_interrupt spawn do - non_nil_handler.call + non_nil_handler.call int_type rescue ex ex.inspect_with_backtrace(STDERR) STDERR.puts("FATAL: uncaught exception while processing interrupt handler, exiting") diff --git a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr index 796369c65a85..fe2fbe381d03 100644 --- a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr @@ -22,8 +22,11 @@ lib LibC pInputControl : Void* ) : BOOL - CTRL_C_EVENT = 0 - CTRL_BREAK_EVENT = 1 + CTRL_C_EVENT = 0 + CTRL_BREAK_EVENT = 1 + CTRL_CLOSE_EVENT = 2 + CTRL_LOGOFF_EVENT = 5 + CTRL_SHUTDOWN_EVENT = 6 alias PHANDLER_ROUTINE = DWORD -> BOOL diff --git a/src/process.cr b/src/process.cr index eb0c165950ce..a1b827d73754 100644 --- a/src/process.cr +++ b/src/process.cr @@ -58,12 +58,46 @@ class Process # * On Unix-like systems, this traps `SIGINT`. # * On Windows, this captures Ctrl + C and # Ctrl + Break signals sent to a console application. + @[Deprecated("Use `#on_terminate` instead")] def self.on_interrupt(&handler : ->) : Nil Crystal::System::Process.on_interrupt(&handler) end + # Installs *handler* as the new handler for termination requests. Removes any + # previously set termination handler. + # + # The handler is executed on a fresh fiber every time an interrupt occurs. + # + # * On Unix-like systems, this traps `SIGINT`, `SIGHUP` and `SIGTERM`. + # * On Windows, this captures Ctrl + C, + # Ctrl + Break, terminal close, windows logoff + # and shutdown signals sent to a console application. + # + # ``` + # wait_channel = Channel(Nil).new + # + # Process.on_terminate do |reason| + # case reason + # when .interrupted? + # puts "terminating gracefully" + # wait_channel.close + # when .terminal_disconnected? + # puts "reloading configuration" + # when .session_ended? + # puts "terminating forcefully" + # Process.exit + # end + # end + # + # wait_channel.receive + # puts "bye" + # ``` + def self.on_terminate(&handler : ::Process::ExitReason ->) : Nil + Crystal::System::Process.on_terminate(&handler) + end + # Ignores all interrupt requests. Removes any custom interrupt handler set - # with `#on_interrupt`. + # with `#on_terminate`. # # * On Windows, interrupts generated by Ctrl + Break # cannot be ignored in this way. diff --git a/src/process/status.cr b/src/process/status.cr index c7b78b1a4583..de29351ff12f 100644 --- a/src/process/status.cr +++ b/src/process/status.cr @@ -16,8 +16,8 @@ enum Process::ExitReason # The process terminated abnormally. # - # * On Unix-like systems, this corresponds to `Signal::ABRT`, `Signal::HUP`, - # `Signal::KILL`, `Signal::QUIT`, and `Signal::TERM`. + # * On Unix-like systems, this corresponds to `Signal::ABRT`, `Signal::KILL`, + # and `Signal::QUIT`. # * On Windows, this corresponds to the `NTSTATUS` value # `STATUS_FATAL_APP_EXIT`. Aborted @@ -79,6 +79,18 @@ enum Process::ExitReason # A `Process::Status` that maps to `Unknown` may map to a different value if # new enum members are added to `ExitReason`. Unknown + + # The process exited due to the user closing the terminal window or ending an ssh session. + # + # * On Unix-like systems, this corresponds to `Signal::HUP`. + # * On Windows, this corresponds to the `CTRL_CLOSE_EVENT` message. + TerminalDisconnected + + # The process exited due to the user logging off or shutting down the OS. + # + # * On Unix-like systems, this corresponds to `Signal::TERM`. + # * On Windows, this corresponds to the `CTRL_LOGOFF_EVENT` and `CTRL_SHUTDOWN_EVENT` messages. + SessionEnded end # The status of a terminated process. Returned by `Process#wait`. @@ -129,8 +141,12 @@ class Process::Status case Signal.from_value?(signal_code) when Nil ExitReason::Signal - when .abrt?, .hup?, .kill?, .quit?, .term? + when .abrt?, .kill?, .quit? ExitReason::Aborted + when .hup? + ExitReason::TerminalDisconnected + when .term? + ExitReason::SessionEnded when .int? ExitReason::Interrupted when .trap? diff --git a/src/signal.cr b/src/signal.cr index 2e085b92311e..50360b73c511 100644 --- a/src/signal.cr +++ b/src/signal.cr @@ -40,8 +40,8 @@ require "crystal/system/signal" # 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 responding to a termination request is +# `Process.on_terminate`. # * 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 @@ -105,7 +105,7 @@ enum Signal : Int32 # 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 + # NOTE: `Process.on_terminate` is preferred over `Signal::INT.trap` as a # portable alternative which also works on Windows. def trap(&handler : Signal ->) : Nil {% if @type.has_constant?("CHLD") %} diff --git a/src/spec.cr b/src/spec.cr index c51ee8831de4..474aa7c5a0dc 100644 --- a/src/spec.cr +++ b/src/spec.cr @@ -129,8 +129,8 @@ module Spec add_split_filter ENV["SPEC_SPLIT"]? {% unless flag?(:wasm32) %} - # TODO(wasm): Enable this once `Process.on_interrupt` is implemented - Process.on_interrupt { abort! } + # TODO(wasm): Enable this once `Process.on_terminate` is implemented + Process.on_terminate { abort! } {% end %} run