Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Process.on_terminate #13694

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e153e8e
pass details on interrupt
stakach Jul 22, 2023
2ad9331
fix win32 handler def
stakach Jul 22, 2023
006ce08
update public interface
stakach Jul 22, 2023
4ef574f
fix windows build
stakach Jul 23, 2023
0fa95aa
add missed windows constants
stakach Jul 23, 2023
7f54152
Merge branch 'master' into improve-process-on_interrupt
stakach Aug 18, 2023
cbd80e9
Merge branch 'master' into improve-process-on_interrupt
stakach Aug 19, 2023
78c27f1
Merge branch 'master' into improve-process-on_interrupt
stakach Aug 22, 2023
1ed9f63
Merge branch 'master' into improve-process-on_interrupt
stakach Sep 13, 2023
f9a6263
Merge branch 'master' into improve-process-on_interrupt
stakach Sep 17, 2023
2044e94
use Process::ExitReason enum
stakach Sep 18, 2023
360a3b5
remove alias
stakach Sep 18, 2023
2533fad
fix windows class vars
stakach Sep 19, 2023
6d94721
Merge branch 'master' into improve-process-on_interrupt
stakach Sep 22, 2023
0604bd2
Merge branch 'master' into improve-process-on_interrupt
stakach Nov 2, 2023
8e43767
Merge branch 'master' into improve-process-on_interrupt
stakach Nov 13, 2023
ee8f672
Merge branch 'master' into improve-process-on_interrupt
stakach Nov 16, 2023
0ea6ecb
win32 backwards compat
stakach Nov 16, 2023
118baff
remove type requirements
stakach Nov 16, 2023
793f0a6
fix wasi compatibility
stakach Nov 16, 2023
5765c73
unix with compatibility
stakach Nov 16, 2023
2352065
fix compilation
stakach Nov 16, 2023
33c9ac6
improve readability of win32 proc type
stakach Nov 16, 2023
0a11eb5
Update src/process.cr
stakach Nov 16, 2023
bdc8541
Update src/process/status.cr
stakach Nov 16, 2023
a2bc368
Update src/process/status.cr
stakach Nov 16, 2023
6bcf166
fix win32 compilation
stakach Nov 16, 2023
d497f08
minor performance improvement
stakach Nov 16, 2023
721444e
Merge branch 'master' into improve-process-on_interrupt
stakach Dec 1, 2023
38c8b51
Merge branch 'master' into improve-process-on_interrupt
stakach Dec 20, 2023
f81bb9b
Merge branch 'master' into improve-process-on_interrupt
stakach Dec 23, 2023
56115e0
Merge branch 'master' into improve-process-on_interrupt
stakach Jan 10, 2024
22a9b34
Merge branch 'master' into improve-process-on_interrupt
stakach Jan 22, 2024
78e8047
deprecate on_interrupt and use on_terminate
stakach Jan 23, 2024
d70ee33
fix call to on_terminate
stakach Jan 23, 2024
f460d5c
fix win32
stakach Jan 23, 2024
a740d4a
improve win32 on_terminate
stakach Jan 23, 2024
757079b
update docs
stakach Jan 23, 2024
79cbc17
change wording in description
stakach Jan 24, 2024
bc66bdb
add example code
stakach Jan 24, 2024
2b91b7d
crystal tool format
stakach Jan 24, 2024
fc279c4
Merge branch 'master' into improve-process-on_interrupt
stakach Jan 30, 2024
79c848d
Merge branch 'master' into improve-process-on_interrupt
stakach Jan 30, 2024
35471c9
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 2, 2024
962d654
fix comments and specs for new enum values
stakach Feb 3, 2024
3ceb646
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 4, 2024
8972255
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 6, 2024
539b998
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 8, 2024
0319fc3
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 9, 2024
66b4ce0
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 12, 2024
f4a3374
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 26, 2024
bfdef38
Merge branch 'master' into improve-process-on_interrupt
stakach Feb 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: IMO it would not necessary to deprecate this method. It's just the backend of Process.on_interrupt and should never be called directly. It doesn't hurt to have the deprecation, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I put it there so it would be simpler to find and remove at a later date. Let me know if you want me to remove

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think it's fine. Thanks!

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
stakach marked this conversation as resolved.
Show resolved Hide resolved
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.
stakach marked this conversation as resolved.
Show resolved Hide resolved
#
# ```
# 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`.
stakach marked this conversation as resolved.
Show resolved Hide resolved
# * 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
Loading