Class: CrashWatch::GdbController

Inherits:
Object
  • Object
show all
Defined in:
lib/crash_watch/gdb_controller.rb

Defined Under Namespace

Classes: ExitInfo

Constant Summary collapse

END_OF_RESPONSE_MARKER =
'--------END_OF_RESPONSE--------'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeGdbController

Returns a new instance of GdbController.



35
36
37
38
# File 'lib/crash_watch/gdb_controller.rb', line 35

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

Instance Attribute Details

#debugObject

Returns the value of attribute debug.



33
34
35
# File 'lib/crash_watch/gdb_controller.rb', line 33

def debug
  @debug
end

Instance Method Details

#all_threads_backtracesObject



125
126
127
# File 'lib/crash_watch/gdb_controller.rb', line 125

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

#attach(pid) ⇒ Object

Raises:

  • (ArgumentError)


99
100
101
102
103
104
# File 'lib/crash_watch/gdb_controller.rb', line 99

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

#call(code) ⇒ Object



106
107
108
109
110
# File 'lib/crash_watch/gdb_controller.rb', line 106

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

#closeObject



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/crash_watch/gdb_controller.rb', line 73

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

#close!Object



89
90
91
92
93
94
95
96
97
# File 'lib/crash_watch/gdb_controller.rb', line 89

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

#closed?Boolean

Returns:

  • (Boolean)


69
70
71
# File 'lib/crash_watch/gdb_controller.rb', line 69

def closed?
  return !@pid
end

#current_threadObject



116
117
118
119
# File 'lib/crash_watch/gdb_controller.rb', line 116

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

#current_thread_backtraceObject



121
122
123
# File 'lib/crash_watch/gdb_controller.rb', line 121

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

#execute(command_string, timeout = nil) ⇒ Object



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
# File 'lib/crash_watch/gdb_controller.rb', line 40

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
  return result
end

#program_counterObject



112
113
114
# File 'lib/crash_watch/gdb_controller.rb', line 112

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

#ruby_backtraceObject



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/crash_watch/gdb_controller.rb', line 129

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?
      return nil
    else
      return result
    end
  else
    return nil
  end
ensure
  if filename
    File.unlink(filename) rescue nil
  end
end

#wait_until_exitObject



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/crash_watch/gdb_controller.rb', line 161

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