class CrashWatch::GdbController

def all_threads_backtraces

def all_threads_backtraces
  execute("thread apply all bt full").strip
end

def attach(pid)

def attach(pid)
  pid = pid.to_s.strip
  raise ArgumentError if pid.empty?
  result = execute("attach #{pid}")
  result !~ /(No such process|Unable to access task|Operation not permitted)/
end

def call(code)

def call(code)
  result = execute("call #{code}")
  result =~ /= (.*)$/
  $1
end

def close

def close
  if !closed?
    begin
      execute("detach", 5)
      execute("quit", 5) if !closed?
    rescue Errno::EPIPE
    end
    if !closed?
      @in.close
      @out.close
      Process.waitpid(@pid)
      @pid = nil
    end
  end
end

def close!

def close!
  if !closed?
    @in.close
    @out.close
    Process.kill('KILL', @pid)
    Process.waitpid(@pid)
    @pid = nil
  end
end

def closed?

def closed?
  !@pid
end

def current_thread

def current_thread
  execute("thread") =~ /Current thread is (.+?) /
  $1
end

def current_thread_backtrace

def current_thread_backtrace
  execute("bt full").strip
end

def execute(command_string, timeout = nil)

def execute(command_string, timeout = nil)
  raise "GDB session is already closed" if !@pid
  puts "gdb write #{command_string.inspect}" if @debug
  @in.puts(command_string)
  @in.puts("echo \\n#{END_OF_RESPONSE_MARKER}\\n")
  done = false
  result = ""
  while !done
    begin
      if select([@out], nil, nil, timeout)
        line = @out.readline
        puts "gdb read #{line.inspect}" if @debug
        if line == "#{END_OF_RESPONSE_MARKER}\n"
          done = true
        else
          result << line
        end
      else
        close!
        done = true
        result = nil
      end
    rescue EOFError
      done = true
    end
  end
  result
end

def find_gdb

def find_gdb
  result = nil
  if ENV['GDB'] && File.executable?(ENV['GDB'])
    result = ENV['GDB']
  else
    ENV['PATH'].to_s.split(/:+/).each do |path|
      filename = "#{path}/gdb"
      if File.file?(filename) && File.executable?(filename)
        result = filename
        break
      end
    end
  end
  puts "Found gdb at: #{result}" if result
  config = defined?(RbConfig) ? RbConfig::CONFIG : Config::CONFIG
  if config['target_os'] =~ /freebsd/ && result == "/usr/bin/gdb"
    # /usr/bin/gdb on FreeBSD is broken:
    # https://github.com/FooBarWidget/crash-watch/issues/1
    # Look for a newer one that's installed from ports.
    puts "#{result} is broken on FreeBSD. Looking for an alternative..."
    result = nil
    ["/usr/local/bin/gdb76", "/usr/local/bin/gdb66"].each do |candidate|
      if File.executable?(candidate)
        result = candidate
        break
      end
    end
    if result.nil?
      raise GdbBroken,
        "*** ERROR ***: '/usr/bin/gdb' is broken on FreeBSD. " +
        "Please install the one from the devel/gdb port instead. " +
        "If you want to use another gdb"
    else
      puts "Found gdb at: #{result}" if result
      result
    end
  elsif result.nil?
    raise GdbNotFound,
      "*** ERROR ***: 'gdb' isn't installed. Please install it first.\n" +
      "       Debian/Ubuntu: sudo apt-get install gdb\n" +
      "RedHat/CentOS/Fedora: sudo yum install gdb\n" +
      "            Mac OS X: please install the Developer Tools or XCode\n" +
      "             FreeBSD: use the devel/gdb port\n"
  else
    result
  end
end

def initialize

def initialize
  @pid, @in, @out = popen_command(find_gdb, "-n", "-q")
  execute("set prompt ")
end

def popen_command(*command)

def popen_command(*command)
  a, b = IO.pipe
  c, d = IO.pipe
  if Process.respond_to?(:spawn)
    args = command.dup
    args << {
      STDIN  => a,
      STDOUT => d,
      STDERR => d,
      :close_others => true
    }
    pid = Process.spawn(*args)
  else
    pid = fork do
      STDIN.reopen(a)
      STDOUT.reopen(d)
      STDERR.reopen(d)
      b.close
      c.close
      exec(*command)
    end
  end
  a.close
  d.close
  b.binmode
  c.binmode
  [pid, b, c]
end

def program_counter

def program_counter
  execute("p/x $pc").gsub(/.* = /, '')
end

def ruby_backtrace

def ruby_backtrace
  filename = "/tmp/gdb-capture.#{@pid}.txt"
  
  orig_stdout_fd_copy = call("(int) dup(1)")
  new_stdout = call(%Q{(void *) fopen("#{filename}", "w")})
  new_stdout_fd = call("(int) fileno(#{new_stdout})")
  call("(int) dup2(#{new_stdout_fd}, 1)")
  
  # Let's hope stdout is set to line buffered or unbuffered mode...
  call("(void) rb_backtrace()")
  
  call("(int) dup2(#{orig_stdout_fd_copy}, 1)")
  call("(int) fclose(#{new_stdout})")
  call("(int) close(#{orig_stdout_fd_copy})")
  
  if File.exist?(filename)
    result = File.read(filename)
    result.strip!
    if result.empty?
      nil
    else
      result
    end
  else
    nil
  end
ensure
  if filename
    File.unlink(filename) rescue nil
  end
end

def wait_until_exit

def wait_until_exit
  execute("break _exit")
  
  signal = nil
  backtraces = nil
  snapshot = nil
  
  while true
    result = execute("continue")
    if result =~ /^Program received signal (.+?),/
      signal = $1
      backtraces = execute("thread apply all bt full").strip
      if backtraces.empty?
        backtraces = execute("bt full").strip
      end
      snapshot = yield(self) if block_given?
      
      # This signal may or may not be immediately fatal; the
      # signal might be ignored by the process, or the process
      # has some clever signal handler that fixes the state,
      # or maybe the signal handler must run some cleanup code
      # before killing the process. Let's find out by running
      # the next machine instruction.
      old_program_counter = program_counter
      result = execute("stepi")
      if result =~ /^Program received signal .+?,/
        # Yes, it was fatal. Here we don't care whether the
        # instruction caused a different signal. The last
        # one is probably what we're interested in.
        return ExitInfo.new(nil, signal, backtraces, snapshot)
      elsif result =~ /^Program (terminated|exited)/ || result =~ /^Breakpoint .*? _exit/
        # Running the next instruction causes the program to terminate.
        # Not sure what's going on but the previous signal and
        # backtrace is probably what we're interested in.
        return ExitInfo.new(nil, signal, backtraces, snapshot)
      elsif old_program_counter == program_counter
        # The process cannot continue but we're not sure what GDB
        # is telling us.
        raise "Unexpected GDB output: #{result}"
      end
      # else:
      # The signal doesn't isn't immediately fatal, so save current
      # status, continue, and check whether the process exits later.
    elsif result =~ /^Program terminated with signal (.+?),/
      if $1 == signal
        # Looks like the signal we trapped earlier
        # caused an exit.
        return ExitInfo.new(nil, signal, backtraces, snapshot)
      else
        return ExitInfo.new(nil, signal, nil, snapshot)
      end
    elsif result =~ /^Breakpoint .*? _exit /
      backtraces = execute("thread apply all bt full").strip
      if backtraces.empty?
        backtraces = execute("bt full").strip
      end
      snapshot = yield(self) if block_given?
      # On OS X, gdb may fail to return from the 'continue' command
      # even though the process exited. Kernel bug? In any case,
      # we put a timeout here so that we don't wait indefinitely.
      result = execute("continue", 10)
      if result =~ /^Program exited with code (\d+)\.$/
        return ExitInfo.new($1.to_i, nil, backtraces, snapshot)
      elsif result =~ /^Program exited normally/
        return ExitInfo.new(0, nil, backtraces, snapshot)
      else
        return ExitInfo.new(nil, nil, backtraces, snapshot)
      end
    elsif result =~ /^Program exited with code (\d+)\.$/
      return ExitInfo.new($1.to_i, nil, nil, nil)
    elsif result =~ /^Program exited normally/
      return ExitInfo.new(0, nil, nil, nil)
    else
      return ExitInfo.new(nil, nil, nil, nil)
    end
  end
end