class Rerun::Runner

Public Class Methods

keep_running(cmd, options) click to toggle source
# File lib/rerun/runner.rb, line 7
def self.keep_running(cmd, options)
  runner = new(cmd, options)
  runner.start
  runner.join
  # apparently runner doesn't keep running anymore (as of Listen 2) so we have to sleep forever :-(
  sleep 10000 while true # :-(
end
new(run_command, options = {}) click to toggle source
# File lib/rerun/runner.rb, line 18
def initialize(run_command, options = {})
  @run_command, @options = run_command, options
  @run_command = "ruby #{@run_command}" if @run_command.split(' ').first =~ /\.rb$/
end

Public Instance Methods

app_name() click to toggle source
# File lib/rerun/runner.rb, line 125
def app_name
  @options[:name]
end
change_message(changes) click to toggle source
# File lib/rerun/runner.rb, line 221
def change_message(changes)
  message = [:modified, :added, :removed].map do |change|
    count = changes[change] ? changes[change].size : 0
    if count > 0
      "#{count} #{change}"
    end
  end.compact.join(", ")

  changed_files = changes.values.flatten
  if changed_files.count > 0
    message += ": "
    message += changes.values.flatten[0..3].map {|path| path.split('/').last}.join(', ')
    if changed_files.count > 3
      message += ", ..."
    end
  end
  message
end
clear?() click to toggle source
# File lib/rerun/runner.rb, line 109
def clear?
  @options[:clear]
end
clear_screen() click to toggle source
# File lib/rerun/runner.rb, line 370
def clear_screen
  # see http://ascii-table.com/ansi-escape-sequences-vt-100.php
  $stdout.print "\033[H\033[2J"
end
die() click to toggle source
# File lib/rerun/runner.rb, line 240
def die
  #stop_keypress_thread   # don't do this since we're probably *in* the keypress thread
  stop # stop the child process if it exists
  exit 0 # todo: status code param
end
dir() click to toggle source
# File lib/rerun/runner.rb, line 93
def dir
  @options[:dir]
end
dirs() click to toggle source
# File lib/rerun/runner.rb, line 97
def dirs
  @options[:dir] || "."
end
exit?() click to toggle source
# File lib/rerun/runner.rb, line 121
def exit?
  @options[:exit]
end
force_polling() click to toggle source
# File lib/rerun/runner.rb, line 136
def force_polling
  @options[:force_polling]
end
git_head_changed?() click to toggle source
# File lib/rerun/runner.rb, line 311
def git_head_changed?
  old_git_head = @git_head
  read_git_head
  @git_head and old_git_head and @git_head != old_git_head
end
ignore() click to toggle source
# File lib/rerun/runner.rb, line 105
def ignore
  @options[:ignore] || []
end
join() click to toggle source
# File lib/rerun/runner.rb, line 246
def join
  @watcher.join
end
key_pressed() click to toggle source

non-blocking stdin reader. returns a 1-char string if a key was pressed; otherwise nil

# File lib/rerun/runner.rb, line 339
def key_pressed
  begin
    # this "raw input" nonsense is because unix likes waiting for linefeeds before sending stdin

    # 'raw' means turn raw input on

    # restore proper output newline handling -- see stty.rb and "man stty" and /usr/include/sys/termios.h
    # looks like "raw" flips off the OPOST bit 0x00000001 /* enable following output processing */
    # which disables #define ONLCR          0x00000002      /* map NL to CR-NL (ala CRMOD) */
    # so this sets it back on again since all we care about is raw input, not raw output
    stty "raw opost"

    c = nil
    if $stdin.ready?
      c = $stdin.getc
    end
    c.chr if c
  ensure
    stty "-raw" # turn raw input off
  end


  # note: according to 'man tty' the proper way restore the settings is
  # tty_state=`stty -g`
  # ensure
  #   system 'stty "#{tty_state}'
  # end
  # but this way seems fine and less confusing

end
notify(title, body, background = true) click to toggle source
# File lib/rerun/runner.rb, line 322
def notify(title, body, background = true)
  Notification.new(title, body, @options).send(background) if @options[:notify]
  puts
  say "#{app_name} #{title}"
end
pattern() click to toggle source
# File lib/rerun/runner.rb, line 101
def pattern
  @options[:pattern]
end
quiet?() click to toggle source
# File lib/rerun/runner.rb, line 113
def quiet?
  @options[:quiet]
end
read_git_head() click to toggle source
# File lib/rerun/runner.rb, line 317
def read_git_head
  git_head_file = File.join(dir, '.git', 'HEAD')
  @git_head = File.exists?(git_head_file) && File.read(git_head_file)
end
restart(with_signal = true) click to toggle source
# File lib/rerun/runner.rb, line 66
def restart(with_signal = true)
  @restarting = true
  if @options[:restart] && with_signal
    restart_with_signal(@options[:signal])
  else
    stop
    start
  end
  @restarting = false
end
restart_with_signal(restart_signal) click to toggle source
# File lib/rerun/runner.rb, line 129
def restart_with_signal(restart_signal)
  if @pid && (@pid != 0)
    notify "restarting", "We will be with you shortly."
    send_signal(restart_signal)
  end
end
run(command) click to toggle source
# File lib/rerun/runner.rb, line 217
def run command
  Kernel.spawn command
end
running?() click to toggle source
# File lib/rerun/runner.rb, line 250
def running?
  send_signal(0)
end
say(msg) click to toggle source
# File lib/rerun/runner.rb, line 328
def say msg
  puts "#{Time.now.strftime("%T")} [rerun] #{msg}" unless quiet?
end
send_signal(signal) click to toggle source

Send the signal to process @pid. @returns true if the signal is sent @returns false if sending the signal fails If sending the signal fails, the exception will be swallowed (and logged if verbose is true) and this method will return false.

# File lib/rerun/runner.rb, line 289
def send_signal(signal)
  say "Sending signal #{signal} to #{@pid}" unless signal == 0 if verbose?
  Process.kill(signal, @pid)
  true
rescue => e
  say "Signal #{signal} failed: #{e.class}: #{e.message}" if verbose?
  false
end
signal_and_wait(signal) click to toggle source

Send the signal to process @pid and wait for it to die. @returns true if the process dies @returns false if either sending the signal fails or the process fails to die

# File lib/rerun/runner.rb, line 257
def signal_and_wait(signal)

  signal_sent = if windows?
                  force_kill = (signal == 'KILL')
                  system("taskkill /T #{'/F' if force_kill} /PID #{@pid}")
                else
                  send_signal(signal)
                end

  if signal_sent
    # the signal was successfully sent, so wait for the process to die
    begin
      timeout(@options[:wait]) do
        Process.wait(@pid)
      end
      process_status = $?
      say "Process ended: #{process_status}" if verbose?
      true
    rescue Timeout::Error
      false
    end
  else
    false
  end
end
start() click to toggle source
# File lib/rerun/runner.rb, line 140
def start
  if @already_running
    taglines = [
      "Here we go again!",
      "Keep on trucking.",
      "Once more unto the breach, dear friends, once more!",
      "The road goes ever on and on, down from the door where it began.",
    ]
    notify "restarted", taglines[rand(taglines.size)]
  else
    taglines = [
      "To infinity... and beyond!",
      "Charge!",
    ]
    notify "launched", taglines[rand(taglines.size)]
    @already_running = true
  end

  clear_screen if clear?
  start_keypress_thread unless @keypress_thread

  begin
    @pid = run @run_command
    say "Rerun (#{$PID}) running #{app_name} (#{@pid})"
  rescue => e
    puts "#{e.class}: #{e.message}"
    exit
  end

  status_thread = Process.detach(@pid) # so if the child exits, it dies

  Signal.trap("INT") do # INT = control-C -- allows user to stop the top-level rerun process
    die
  end

  Signal.trap("TERM") do # TERM is the polite way of terminating a process
    die
  end

  begin
    sleep 2
  rescue Interrupt => e
    # in case someone hits control-C immediately ("oops!")
    die
  end

  if exit?
    status = status_thread.value
    if status.success?
      notify "succeeded", ""
    else
      notify "failed", "Exit status #{status.exitstatus}"
    end
  else
    if !running?
      notify "Launch Failed", "See console for error output"
      @already_running = false
    end
  end

  unless @watcher

    watcher = Watcher.new(:directory => dirs, :pattern => pattern, :ignore => ignore, :force_polling => force_polling) do |changes|

      message = change_message(changes)

      say "Change detected: #{message}"
      restart unless @restarting
    end
    watcher.start
    @watcher = watcher
    say "Watching #{dir.join(', ')} for #{pattern}" +
          (ignore.empty? ? "" : " (ignoring #{ignore.join(',')})") +
          (watcher.adapter.nil? ? "" : " with #{watcher.adapter_name} adapter")
  end
end
start_keypress_thread() click to toggle source
# File lib/rerun/runner.rb, line 23
def start_keypress_thread
  return if @options[:background]

  @keypress_thread = Thread.new do
    while true
      if c = key_pressed
        case c.downcase
        when 'c'
          say "Clearing screen"
          clear_screen
        when 'r'
          say "Restarting"
          restart
        when 'f'
          say "Stopping and starting"
          restart(false)
        when 'p'
          toggle_pause
        when 'x', 'q'
          die
          break # the break will stop this thread, in case the 'die' doesn't
        else
          puts "\n#{c.inspect} pressed inside rerun"
          puts [["c", "clear screen"],
                ["r", "restart"],
                ["f", "forced restart (stop and start)"],
                ["p", "toggle pause"],
                ["x or q", "stop and exit"]
               ].map {|key, description| "  #{key} -- #{description}"}.join("\n")
          puts
        end
      end
      sleep 1 # todo: use select instead of polling somehow?
    end
  end
  @keypress_thread.run
end
stop() click to toggle source

todo: test escalation

# File lib/rerun/runner.rb, line 299
def stop
  if @pid && (@pid != 0)
    notify "stopping", "All good things must come to an end." unless @restarting
    @options[:signal].split(',').each do |signal|
      success = signal_and_wait(signal)
      return true if success
    end
  end
rescue => e
  false
end
stop_keypress_thread() click to toggle source
# File lib/rerun/runner.rb, line 61
def stop_keypress_thread
  @keypress_thread.kill if @keypress_thread
  @keypress_thread = nil
end
stty(args) click to toggle source
# File lib/rerun/runner.rb, line 332
def stty(args)
  system "stty #{args}"
end
toggle_pause() click to toggle source
# File lib/rerun/runner.rb, line 77
def toggle_pause
  unless @pausing
    say "Pausing.  Press 'p' again to resume."
    @watcher.pause
    @pausing = true
  else
    say "Resuming."
    @watcher.unpause
    @pausing = false
  end
end
unpause() click to toggle source
# File lib/rerun/runner.rb, line 89
def unpause
  @watcher.unpause
end
verbose?() click to toggle source
# File lib/rerun/runner.rb, line 117
def verbose?
  @options[:verbose]
end