Module: Trepanning

Includes:
RemoteCommunication
Defined in:
app/iseq.rb,
app/run.rb,
app/file.rb,
app/client.rb

Overview

Copyright © 2011 Rocky Bernstein <[email protected]> Things related to RubyVM::InstructionSequence’s

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.debug_program(dbgr, program_to_debug) ⇒ Object

Given a Ruby interpreter and program we are to debug, debug it. The caller must ensure that ARGV is set up to remove any debugger arguments or things that the debugged program isn’t supposed to see. FIXME: Should we make ARGV an explicit parameter?



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'app/run.rb', line 12

def debug_program(dbgr, program_to_debug)

  # Make sure Ruby script syntax checks okay.
  # Otherwise we get a load message that looks like trepanning has 
  # a problem. 
  output = ruby_syntax_errors(program_to_debug)
  if output
    puts output
    exit $?.exitstatus 
  end

  dbgr.trace_filter << self.method(:debug_program)
  dbgr.trace_filter << Kernel.method(:load)

  old_dollar_0 = $0

  # Without the dance below to set $0, setting it to a signifcantly
  # longer value will truncate it in some OS's. See
  # http://www.ruby-forum.com/topic/187083
  $progname = program_to_debug
  alias $0 $progname
  dollar_0_tracker = lambda {|val| $program_name = val} 
  trace_var(:$0, dollar_0_tracker)

  dbgr.debugger(:hide_stack=>true) do
    dbgr.core.processor.hidelevels[Thread.current] = 
      RubyVM::Frame.current.stack_size + 1
    begin
      Kernel::load program_to_debug
    rescue Interrupt
    end
  end

  # The dance we have to undo to restore $0 and undo the mess created
  # above.
  $0 = old_dollar_0
  untrace_var(:$0, dollar_0_tracker)
rescue
  if dbgr.settings[:post_mortem]
    frame = RubyVM::Frame.current.prev(0)
    dbgr.core.step_count = 0  # Make event processor stop
    dbgr.core.processor.settings[:debugstack] = 0  # Make event processor stop
    dbgr.core.event_processor('post-mortem', frame, $!)
  else
    raise
  end
end

.ruby_syntax_errors(prog_script) ⇒ Object



76
77
78
79
80
81
82
# File 'app/run.rb', line 76

def ruby_syntax_errors(prog_script)
  output = `#{RbConfig.ruby} -c #{prog_script.inspect} 2>&1`
  if $?.exitstatus != 0 and RUBY_PLATFORM !~ /mswin/
    return output
  end
  return nil
end

.start_client(options) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/client.rb', line 9

def start_client(options)
  puts "Client option given"
  user_opts = {}
  %w(readline).each do |opt|
    user_opts[opt.to_sym] = options[opt.to_sym]
  end
  dbgr = Trepan.new(:client      => true,
                    :cmdfiles    => [],
                    :initial_dir => options[:chdir],
                    :nx          => true,
                    :host        => options[:host],
                    :port        => options[:port],
                    :user_opts   => user_opts
                    )
  intf = dbgr.intf[-1]
  intf.write_remote(SYNC, 'FIXME: add useful info')
  while true
    begin
      control_code, line = intf.read_remote
    rescue EOFError, Errno::EPIPE
      puts "Remote debugged process closed connection"
      break
    end
    # p [control_code, line]
    case control_code
    when PRINT

      # FIXME: don't know why server sometimes adds a gratuituous space.
      # the space is added somewhere inside TCPSocket.print
      line = line[0..-2] if line.end_with?("\n ")

      print line
    when CONFIRM_TRUE
      response = intf.confirm(line, true)
      intf.write_remote(CONFIRM_REPLY, response ? 'Y' : 'N')
    when CONFIRM_FALSE
      response = intf.confirm(line, true)
      intf.write_remote(CONFIRM_REPLY, response ? 'Y' : 'N')
    when PROMPT
      # Printing of prompt has been handled already by PRINT.
      begin
        command = intf.read_command(line)
      rescue EOFError
        puts "user-side EOF. Quitting..."
        break
      end
      begin 
        intf.write_remote(COMMAND, command)
      rescue Errno::EPIPE
        puts "Remote debugged process died"
        break
      end
    when QUIT
      break
    when RESTART
      break
    else
      $stderr.puts "** Unknown control code: #{control_code}"
    end
  end
end

.whence_file(prog_script) ⇒ Object

Do a shell-like path lookup for prog_script and return the results. If we can’t find anything return prog_script.



62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'app/run.rb', line 62

def whence_file(prog_script)
  if prog_script.start_with?(File::SEPARATOR) || prog_script.start_with?('.')
    # Don't search since this name has path is explicitly absolute or
    # relative.
    return prog_script
  end
  for dirname in ENV['PATH'].split(File::PATH_SEPARATOR) do
    prog_script_try = File.join(dirname, prog_script)
    return prog_script_try if File.readable?(prog_script_try)
  end
  # Failure
  return prog_script
end

Instance Method Details

#file_match_pat(filename) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'app/file.rb', line 13

def file_match_pat(filename)
  prefix = 
    if filename[0..0] == File::SEPARATOR
      # An absolute filename has to match at the beginning and
      # the end.
      '^'
    else
      # An nonabsolute filename has to match either at the
      # beginning of the file name or have a path separator before
      # the supplied part, e.g. "file.rb" does not match "myfile.rb"
      # but matches "my/file.rb"
      '(?:^|[/])'
    end
  "#{prefix}#{Regexp.escape(filename)}$"
end

#filter_scripts(dirname) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/file.rb', line 29

def filter_scripts(dirname)
  match_block = Proc.new{|filename, iseq| filename =~ /^#{dirname}/}
  scripts = SCRIPT_ISEQS__.select(&match_block)
  SCRIPT_ISEQS__.delete_if(&match_block)
  match_block = Proc.new{|iseq| 
    iseq.source_container[1] =~ /^#{dirname}/
  }
  rejected = {}
  # SCRIPT_ISEQS__ is updated automatically.  Dup copy is to make
  # sure we we aren't iterating over something that some other
  # process, thread or hook is filling.
  script_iseqs = SCRIPT_ISEQS__.dup
  script_iseqs.each do |name, iseqs|
    ary = iseqs.select(&match_block)
    rejected[name] = ary unless ary.empty?
    iseqs.delete_if(&match_block)
  end
  return [scripts, rejected]
end

#find_iseq_with_line_from_iseq(iseq, lineno, go_up = true) ⇒ Object

Returns a RubyVM::Instruction for the specified line. We search the current instruction sequence iseq and then up the parent scope. If we hit the top and we can’t find line that way, then we reverse the search from the top and search down. This will add all siblings of ancestors of meth. Similar to rbx-trepanning method “find_method_with_line”.



12
13
14
# File 'app/iseq.rb', line 12

def find_iseq_with_line_from_iseq(iseq, lineno, go_up=true)
  find_iseq_with_line_from_iseq2(iseq, lineno, go_up, {})
end

#find_iseq_with_line_from_iseq2(iseq, lineno, go_up, seen) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'app/iseq.rb', line 16

def find_iseq_with_line_from_iseq2(iseq, lineno, go_up, seen)
  return iseq if iseq.offsetlines.values.flatten.uniq.member?(lineno)
  seen[iseq] = true
  prev_iseq = iseq
  while prev_iseq = prev_iseq.parent
    iseq = prev_iseq
    return iseq if iseq.offsetlines.values.flatten.uniq.member?(lineno)
  end if go_up
  # At top and not found so now go down..
  iseq.child_iseqs.each do |child_iseq|
    next if seen[child_iseq] # we tried before
    # puts "#{child_iseq.name}, #{child_iseq}"
    result = find_iseq_with_line_from_iseq2(child_iseq, lineno, false, seen)
    return result if result
  end
  return nil
end

#find_iseqs(iseqs_hash, name) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
# File 'app/file.rb', line 49

def find_iseqs(iseqs_hash, name)
  iseq_name, filename = name.split(/@/)
  return [] unless iseqs_hash.member?(iseq_name)
  iseqs = iseqs_hash[iseq_name]
  # FIXME: filter out debugger iseqs
  if filename
    filename_pat = file_match_pat(filename)
    iseqs.select{|iseq| iseq.source_container[1] =~ /#{filename_pat}/}
  else
    return iseqs 
  end
end

#find_iseqs_with_lineno(filename, lineno) ⇒ Object



62
63
64
65
66
67
68
69
70
71
# File 'app/file.rb', line 62

def find_iseqs_with_lineno(filename, lineno)
  files = find_scripts(filename)
  files.each do |file|
    SCRIPT_ISEQS__[file].each do |iseq|
      found_iseq = find_iseq_with_line_from_iseq(iseq, lineno, true)
      return found_iseq if found_iseq
    end
  end
  return nil
end

#find_scripts(filename) ⇒ Object



73
74
75
76
# File 'app/file.rb', line 73

def find_scripts(filename)
  filename_pat = file_match_pat(filename)
  return SCRIPT_LINES__.keys.grep(/#{filename_pat}/)
end