Skip to content

Commit

Permalink
Add Process.on_interrupt (#13034)
Browse files Browse the repository at this point in the history
Co-authored-by: Johannes Müller <[email protected]>
  • Loading branch information
HertzDevil and straight-shoota authored Feb 4, 2023
1 parent 7aefd61 commit 8853f73
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 9 deletions.
2 changes: 1 addition & 1 deletion samples/sdl/raytracer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def render(scene, surface)
surface.update_rect 0, 0, 0, 0
end

Signal::INT.trap { exit }
Process.on_interrupt { exit }

scene = Scene.new(
[
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 @@ -317,6 +317,14 @@ describe Process do
end
end

describe ".on_interrupt" do
it "compiles" do
typeof(Process.on_interrupt { })
typeof(Process.ignore_interrupts!)
typeof(Process.restore_interrupts!)
end
end

describe "#signal" do
pending_win32 "kills a process" do
process = Process.new(*standing_command)
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,10 @@ class Crystal::Command
begin
elapsed = Time.measure do
Process.run(output_filename, args: run_args, input: Process::Redirect::Inherit, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) do |process|
{% unless flag?(:win32) || flag?(:wasm32) %}
{% unless flag?(:wasm32) %}
# Ignore the signal so we don't exit the running process
# (the running process can still handle this signal)
::Signal::INT.ignore # do
Process.ignore_interrupts!
{% end %}
end
end
Expand Down
16 changes: 16 additions & 0 deletions src/crystal/atomic_semaphore.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# :nodoc:
class Crystal::AtomicSemaphore
@m = Atomic(UInt32).new(0)

def wait(&) : Nil
m = @m.get
while m == 0 || !@m.compare_and_set(m, m &- 1).last
yield
m = @m.get
end
end

def signal : Nil
@m.add(1)
end
end
14 changes: 14 additions & 0 deletions src/crystal/system/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ struct Crystal::System::Process
# Sends a *signal* to the processes identified by the given *pids*.
# def self.signal(pid : Int, signal : Int)

# Installs *handler* as the new handler for interrupt requests. Removes any
# previously set interrupt handler.
# def self.on_interrupt(&handler : ->)

# Ignores all interrupt requests. Removes any custom interrupt handler set
# def self.ignore_interrupts!

# Restores default handling of interrupt requests.
# def self.restore_interrupts!

# Spawns a fiber responsible for executing interrupt handlers on the main
# thread.
# def self.start_interrupt_loop

# Whether the process identified by *pid* is still registered in the system.
# def self.exists?(pid : Int) : Bool

Expand Down
16 changes: 16 additions & 0 deletions src/crystal/system/unix/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ struct Crystal::System::Process
raise RuntimeError.from_errno("kill") if ret < 0
end

def self.on_interrupt(&handler : ->) : Nil
::Signal::INT.trap { |_signal| handler.call }
end

def self.ignore_interrupts! : Nil
::Signal::INT.ignore
end

def self.restore_interrupts! : Nil
::Signal::INT.reset
end

def self.start_interrupt_loop : Nil
# do nothing; `Crystal::Signal.start_loop` takes care of this
end

def self.exists?(pid)
ret = LibC.kill(pid, 0)
if ret == 0
Expand Down
15 changes: 15 additions & 0 deletions src/crystal/system/wasi/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ struct Crystal::System::Process
raise NotImplementedError.new("Process.signal")
end

def self.on_interrupt(&handler : ->) : Nil
raise NotImplementedError.new("Process.on_interrupt")
end

def self.ignore_interrupts! : Nil
raise NotImplementedError.new("Process.ignore_interrupts!")
end

def self.restore_interrupts! : Nil
raise NotImplementedError.new("Process.restore_interrupts!")
end

def self.start_interrupt_loop : Nil
end

def self.exists?(pid)
raise NotImplementedError.new("Process.exists?")
end
Expand Down
55 changes: 55 additions & 0 deletions src/crystal/system/win32/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ require "c/processthreadsapi"
require "c/handleapi"
require "c/synchapi"
require "process/shell"
require "crystal/atomic_semaphore"

struct Crystal::System::Process
getter pid : LibC::DWORD
@thread_id : LibC::DWORD
@process_handle : LibC::HANDLE

@@interrupt_handler : Proc(Nil)?
@@interrupt_count = Crystal::AtomicSemaphore.new
@@win32_interrupt_handler : LibC::PHANDLER_ROUTINE?
@@setup_interrupt_handler = Atomic::Flag.new

def initialize(process_info)
@pid = process_info.dwProcessId
@thread_id = process_info.dwThreadId
Expand Down Expand Up @@ -72,6 +78,55 @@ struct Crystal::System::Process
raise NotImplementedError.new("Process.signal")
end

def self.on_interrupt(&@@interrupt_handler : ->) : 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)
@@interrupt_count.signal
1
end
LibC.SetConsoleCtrlHandler(handler, 1)
end

def self.ignore_interrupts! : Nil
remove_interrupt_handler
LibC.SetConsoleCtrlHandler(nil, 1)
end

def self.restore_interrupts! : Nil
remove_interrupt_handler
LibC.SetConsoleCtrlHandler(nil, 0)
end

private def self.remove_interrupt_handler
if old = @@win32_interrupt_handler
LibC.SetConsoleCtrlHandler(old, 0)
@@win32_interrupt_handler = nil
end
end

def self.start_interrupt_loop : Nil
return unless @@setup_interrupt_handler.test_and_set

spawn(name: "Interrupt signal loop") do
while true
@@interrupt_count.wait { sleep 50.milliseconds }

if handler = @@interrupt_handler
non_nil_handler = handler # if handler is closured it will also have the Nil type
spawn do
non_nil_handler.call
rescue ex
ex.inspect_with_backtrace(STDERR)
STDERR.puts("FATAL: uncaught exception while processing interrupt handler, exiting")
STDERR.flush
LibC._exit(1)
end
end
end
end
end

def self.exists?(pid)
handle = LibC.OpenProcess(LibC::PROCESS_QUERY_INFORMATION, 0, pid)
return false if handle.nil?
Expand Down
4 changes: 3 additions & 1 deletion src/kernel.cr
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,9 @@ end
end
end

{% unless flag?(:win32) %}
{% if flag?(:win32) %}
Crystal::System::Process.start_interrupt_loop
{% else %}
Signal.setup_default_handlers
{% end %}

Expand Down
7 changes: 7 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/consoleapi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ lib LibC

fun GetConsoleCP : DWORD
fun GetConsoleOutputCP : DWORD

CTRL_C_EVENT = 0
CTRL_BREAK_EVENT = 1

alias PHANDLER_ROUTINE = DWORD -> BOOL

fun SetConsoleCtrlHandler(handlerRoutine : PHANDLER_ROUTINE, add : BOOL) : BOOL
end
26 changes: 26 additions & 0 deletions src/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,32 @@ class Process
Crystal::System::Process.signal(pid, signal.value)
end

# Installs *handler* as the new handler for interrupt requests. Removes any
# previously set interrupt handler.
#
# The handler is executed on a fresh fiber every time an interrupt occurs.
#
# * 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.
def self.on_interrupt(&handler : ->) : Nil
Crystal::System::Process.on_interrupt(&handler)
end

# Ignores all interrupt requests. Removes any custom interrupt handler set
# with `#on_interrupt`.
#
# * On Windows, interrupts generated by <kbd>Ctrl</kbd> + <kbd>Break</kbd>
# cannot be ignored in this way.
def self.ignore_interrupts! : Nil
Crystal::System::Process.ignore_interrupts!
end

# Restores default handling of interrupt requests.
def self.restore_interrupts! : Nil
Crystal::System::Process.restore_interrupts!
end

# Returns `true` if the process identified by *pid* is valid for
# a currently registered process, `false` otherwise. Note that this
# returns `true` for a process in the zombie or similar state.
Expand Down
6 changes: 4 additions & 2 deletions src/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ require "c/unistd"
# sleep 3
# ```
#
# Note:
# - An uncaught exception in a signal handler is a fatal error.
# NOTE: `Process.on_interrupt` is preferred over `Signal::INT.trap`, as the
# former also works on Windows.
#
# WARNING: An uncaught exception in a signal handler is a fatal error.
enum Signal : Int32
HUP = LibC::SIGHUP
INT = LibC::SIGINT
Expand Down
6 changes: 3 additions & 3 deletions src/spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ end

Spec.add_split_filter ENV["SPEC_SPLIT"]?

{% unless flag?(:win32) || flag?(:wasm32) %}
# TODO(windows): re-enable this once Signal is ported
Signal::INT.trap { Spec.abort! }
{% unless flag?(:wasm32) %}
# TODO(wasm): Enable this once `Process.on_interrupt` is implemented
Process.on_interrupt { Spec.abort! }
{% end %}

Spec.run

0 comments on commit 8853f73

Please sign in to comment.