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