Skip to content

Commit

Permalink
Add Process.on_terminate (#13694)
Browse files Browse the repository at this point in the history
Co-authored-by: Devonte W <devnote.dev75@gmail.com>
Co-authored-by: Johannes Müller <straightshoota@gmail.com>
  • Loading branch information
3 people committed Feb 27, 2024
1 parent 54a3279 commit c6bae60
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 18 deletions.
10 changes: 8 additions & 2 deletions spec/std/process/status_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions spec/std/process_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
4 changes: 4 additions & 0 deletions src/crystal/system/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down
25 changes: 25 additions & 0 deletions src/crystal/system/unix/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/crystal/system/wasi/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 22 additions & 4 deletions src/crystal/system/win32/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
7 changes: 5 additions & 2 deletions src/lib_c/x86_64-windows-msvc/c/consoleapi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 35 additions & 1 deletion src/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,46 @@ class Process
# * On Unix-like systems, this traps `SIGINT`.
# * On Windows, this captures <kbd>Ctrl</kbd> + <kbd>C</kbd> and
# <kbd>Ctrl</kbd> + <kbd>Break</kbd> 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 <kbd>Ctrl</kbd> + <kbd>C</kbd>,
# <kbd>Ctrl</kbd> + <kbd>Break</kbd>, 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 <kbd>Ctrl</kbd> + <kbd>Break</kbd>
# cannot be ignored in this way.
Expand Down
22 changes: 19 additions & 3 deletions src/process/status.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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?
Expand Down
6 changes: 3 additions & 3 deletions src/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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") %}
Expand Down
4 changes: 2 additions & 2 deletions src/spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c6bae60

Please sign in to comment.