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 117
def app_name
  @options[:name]
end
change_message(changes) click to toggle source
# File lib/rerun/runner.rb, line 212
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 333
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 231
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 113
def exit?
  @options[:exit]
end
force_polling() click to toggle source
# File lib/rerun/runner.rb, line 128
def force_polling
  @options[:force_polling]
end
git_head_changed?() click to toggle source
# File lib/rerun/runner.rb, line 279
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 237
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 303
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
    system("stty raw opost")

    c = nil
    if $stdin.ready?
      c = $stdin.getc
    end
    c.chr if c
  ensure
    system "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 290
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
read_git_head() click to toggle source
# File lib/rerun/runner.rb, line 285
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() click to toggle source
# File lib/rerun/runner.rb, line 62
def restart
  @restarting = true
  if @options[:restart]
    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 121
def restart_with_signal(restart_signal)
  if @pid && (@pid != 0)
    notify "restarting", "We will be with you shortly."
    signal(restart_signal)
  end
end
run(command) click to toggle source
# File lib/rerun/runner.rb, line 208
def run command
  Kernel.spawn command
end
running?() click to toggle source
# File lib/rerun/runner.rb, line 241
def running?
  signal(0)
end
say(msg) click to toggle source
# File lib/rerun/runner.rb, line 296
def say msg
  puts "#{Time.now.strftime("%T")} [rerun] #{msg}"
end
signal(signal) click to toggle source
# File lib/rerun/runner.rb, line 245
def signal(signal)
  say "Sending signal #{signal} to #{@pid}" unless signal == 0
  Process.kill(signal, @pid)
  true
rescue
  false
end
start() click to toggle source
# File lib/rerun/runner.rb, line 132
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
  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 'p'
          toggle_pause if watcher_running?
        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"],
                ["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 254
def stop
  default_signal = @options[:signal] || "TERM"
  if @pid && (@pid != 0)
    notify "stopping", "All good things must come to an end." unless @restarting
    begin
      timeout(5) do # todo: escalation timeout setting
        # start with a polite SIGTERM
        signal(default_signal) && Process.wait(@pid)
      end
    rescue Timeout::Error
      begin
        timeout(5) do
          # escalate to SIGINT aka control-C since some foolish process may be ignoring SIGTERM
          signal("INT") && Process.wait(@pid)
        end
      rescue Timeout::Error
        # escalate to SIGKILL aka "kill -9" which cannot be ignored
        signal("KILL") && Process.wait(@pid)
      end
    end
  end
rescue => e
  false
end
stop_keypress_thread() click to toggle source
# File lib/rerun/runner.rb, line 57
def stop_keypress_thread
  @keypress_thread.kill if @keypress_thread
  @keypress_thread = nil
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
watcher_running?() click to toggle source
# File lib/rerun/runner.rb, line 73
def watcher_running?
  @watcher && @watcher.running?
end