From 8853f7333f1e5be8f61a509a427f741bbe9ddb85 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 4 Feb 2023 20:51:10 +0800 Subject: [PATCH] Add `Process.on_interrupt` (#13034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- samples/sdl/raytracer.cr | 2 +- spec/std/process_spec.cr | 8 +++ src/compiler/crystal/command.cr | 4 +- src/crystal/atomic_semaphore.cr | 16 ++++++ src/crystal/system/process.cr | 14 +++++ src/crystal/system/unix/process.cr | 16 ++++++ src/crystal/system/wasi/process.cr | 15 +++++ src/crystal/system/win32/process.cr | 55 +++++++++++++++++++ src/kernel.cr | 4 +- src/lib_c/x86_64-windows-msvc/c/consoleapi.cr | 7 +++ src/process.cr | 26 +++++++++ src/signal.cr | 6 +- src/spec.cr | 6 +- 13 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 src/crystal/atomic_semaphore.cr diff --git a/samples/sdl/raytracer.cr b/samples/sdl/raytracer.cr index 0528272d8cba..6bcea6383a21 100644 --- a/samples/sdl/raytracer.cr +++ b/samples/sdl/raytracer.cr @@ -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( [ diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 82265dd94852..cc1253bf9038 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -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) diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index ca66b460d83b..940a03bad1c3 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -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 diff --git a/src/crystal/atomic_semaphore.cr b/src/crystal/atomic_semaphore.cr new file mode 100644 index 000000000000..c0d15737763c --- /dev/null +++ b/src/crystal/atomic_semaphore.cr @@ -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 diff --git a/src/crystal/system/process.cr b/src/crystal/system/process.cr index 77577a9e5c6b..d613d7ac431c 100644 --- a/src/crystal/system/process.cr +++ b/src/crystal/system/process.cr @@ -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 diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index 9f95136d6229..f94835887a4e 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -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 diff --git a/src/crystal/system/wasi/process.cr b/src/crystal/system/wasi/process.cr index d0b3769c8b72..5951fa1c816b 100644 --- a/src/crystal/system/wasi/process.cr +++ b/src/crystal/system/wasi/process.cr @@ -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 diff --git a/src/crystal/system/win32/process.cr b/src/crystal/system/win32/process.cr index e6d46671361d..c934c5b26047 100644 --- a/src/crystal/system/win32/process.cr +++ b/src/crystal/system/win32/process.cr @@ -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 @@ -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? diff --git a/src/kernel.cr b/src/kernel.cr index fe7595ddcc96..45df75ef1dbe 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -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 %} 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 b5e7ac1ebf42..680e199be2ab 100644 --- a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr @@ -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 diff --git a/src/process.cr b/src/process.cr index 2823ff7b17cc..24bd8b005200 100644 --- a/src/process.cr +++ b/src/process.cr @@ -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 Ctrl + C and + # Ctrl + Break 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 Ctrl + Break + # 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. diff --git a/src/signal.cr b/src/signal.cr index d1352f1f9f0d..9335619d043f 100644 --- a/src/signal.cr +++ b/src/signal.cr @@ -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 diff --git a/src/spec.cr b/src/spec.cr index 5d370e71811a..e9cf5d448efd 100644 --- a/src/spec.cr +++ b/src/spec.cr @@ -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