Top Level Namespace

Constant Summary collapse

EmCommands =
Pry::CommandSet.new do

  create_command /\s*em\s*([0-9\.]*)\s*:(.*)/ do

    description "Wait for a deferrable for a length of time (default 3 seconds). `em 3: EM::HttpRequest.new(url).get`"
    options(
      :keep_retval  => true,
      :interpolate  => false,
      :listing      => "em",
      :requires_gem => 'eventmachine'
    )

    def process(timeout, source)
      # We store the retval and the em-state in globals so that we can catch exceptions
      # raised in the event loop and pass them back pretending to the user that the
      # exception was caused by their currently executing command.
      #
      # We don't want to keep turning the reactor on and off, as that would limit some
      # of the things the user might want to do.
      @@retval, @@em_state = [nil, :waiting]

      # Boot EM before eval'ing the source as it's likely to depend on the reactor.
      run_em_if_necessary!

      # This can happen for example if you do:
      #  em: EM::HttpRequest.new("http://www.google.com/").get.callback{ binding.pry }
      # There ought to be a solution, but it will involve shunting either Pry or EM
      # onto a new thread.
      if EM.reactor_thread == Thread.current
        raise "Could not wait for deferrable, you're in the EM thread!
                If you don't know what to do, try `cd ..`, or just hit ctrl-C until it dies."
      end

      deferrable = target.eval(source)

      # TODO: Allow the user to configure the default timeout
      timeout = timeout == "" ? 3 : Float(timeout)

      wait_for_deferrable(deferrable, timeout) unless deferrable.nil?
    end

    # Boot a new EventMachine reactor into another thread.
    # This allows us to continue to interact with the user on the front-end thread,
    # while they run event-machine commands in the background.
    def run_em_if_necessary!
      require 'eventmachine' unless defined?(EM)
      unless EM.reactor_running?
        Thread.new do
          EM.error_handler{ |e| handle_unexpected_error(e) }
          begin
            EM.run
          rescue Pry::RescuableException => e
            handle_unexpected_error(e)
          end
        end
      end
      sleep 0.01 until EM.reactor_running?
    end

    # If we were the ones to start the EM reactor, we want to catch
    # any exceptions that are raised therein and tell the user about
    # them.
    #
    # If they are still waiting for an async event, assume that this
    # was in relation to that.
    #
    # If not, just print out the error and hope they don't get too
    # confused.
    def handle_unexpected_error(e)
      if waiting?
        @@em_state = :em_error
        @@retval = e
      else
        output.puts "Unexpected exception from EventMachine reactor"
        _pry_.last_exception = e
        _pry_.show_result(e)
      end
    rescue => e
      puts e
    end

    # Run a deferrable on an EM reactor in a different thread,
    # sleep until it has finished, and then return the result.
    #
    # The result is defined to be as useful to an interactive shell user as possible:
    #
    # If the deferrable succeeds or fails with one argument, that one argument is
    # returned; though if it succeeds or fails with many arguments, an array is returned.
    #
    def wait_for_deferrable(deferrable, timeout)

      EM::Timer.new(timeout) { @@em_state = :timeout if waiting? }

      [:callback, :errback].each do |method|
        begin
          deferrable.__send__ method do |*result|
            @@em_state = method
            @@retval = result.size > 1 ? result : result.first
          end
        rescue NoMethodError
          output.puts "WARNING: is not deferrable? #{deferrable}"
          break
        end
      end

      sleep 0.01 until @@em_state != :waiting

      raise "Timeout after #{timeout} seconds" if @@em_state == :timeout

      # Use Pry's (admittedly nascent) handling for exceptions where possible.
      raise @@retval if @@em_state != :callback && Exception === @@retval

      # TODO: This doesn't interact well with the pager.
      output.print "#{@@em_state} " #=>
      @@retval

    # If the main thread is interrupted we must ensure that the @@em_state
    # is no-longer :waiting so that handle_unexpected_error can do the
    # right thing.
    ensure
      @@em_state = :interrupted if waiting?
    end

    def waiting?
      @@em_state == :waiting
    end
  end
end