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

Platform-independent interrupt handlers #12224

Closed
HertzDevil opened this issue Jul 8, 2022 · 0 comments · Fixed by #13034
Closed

Platform-independent interrupt handlers #12224

HertzDevil opened this issue Jul 8, 2022 · 0 comments · Fixed by #13034

Comments

@HertzDevil
Copy link
Contributor

In #7339 (comment) I mentioned a new method Process.on_interrupt as a platform-independent replacement of Signal::INT.trap and friends. Here, an interrupt is defined to be whatever Ctrl+C in a terminal produces. On Windows the method is backed by SetControlCtrlHandler:

# consoleapi.cr
lib LibC
  CTRL_C_EVENT = 0

  alias PHANDLER_ROUTINE = DWORD -> BOOL

  fun SetConsoleCtrlHandler(handlerRoutine : PHANDLER_ROUTINE, add : BOOL) : BOOL
end

class Process
  @@handler = Proc(Nil).new { }
  @@interrupt_handler : LibC::PHANDLER_ROUTINE?

  def self.on_interrupt(&@@handler : ->) : Nil
    @@interrupt_handler.try { |handler| LibC.SetConsoleCtrlHandler(handler, 0) }
    @@interrupt_handler = handler = LibC::PHANDLER_ROUTINE.new do |dwCtrlType|
      next 0 unless dwCtrlType == LibC::CTRL_C_EVENT
      begin
        @@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
      1
    end
    LibC.SetConsoleCtrlHandler(handler, 1)
  end

  # this always disables the user-defined interrupt handler, so
  # perhaps a different method name is more suitable
  def self.on_interrupt(ignore : Bool) : Nil
    @@interrupt_handler.try { |handler| LibC.SetConsoleCtrlHandler(handler, 0) }
    LibC.SetConsoleCtrlHandler(nil, ignore ? 1 : 0)
  end
end

On Unix-like systems, these methods simply forward to Signal::INT:

class Process
  def self.on_interrupt(ignore : Bool) : Nil
    ignore ? Signal::INT.ignore : Signal::INT.reset
  end

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

Spec runners for example can be stopped early by an interrupt request, after which they print all the already executed spec results and then exit. On Windows this does not happen yet because the Signal::INT.trap call is wrapped inside a conditional macro, but this method makes it possible.

This minimal implementation is inadequate because the interrupt handler always runs on a newly spun thread on Windows. We definitely don't want to suddenly force people to start writing thread-safe code, when fiber safety suffices for Signal::INT. Ideally, the handler should be executed on a dedicated fiber on the same thread as the one that called Process.on_interrupt. Is this possible?

Also I am wondering if the exit status in case of an uncaught exception should be 0x40000015 (STATUS_FATAL_APP_EXIT) rather than 1. (This applies to other similar abnormal terminations in the standard library too.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant