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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeGdbController

Returns a new instance of GdbController.



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

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

Instance Attribute Details

#debugObject

Returns the value of attribute debug.



22
23
24
# File 'lib/crash_watch/gdb_controller.rb', line 22

def debug
  @debug
end

Class Method Details

.gdb_installed?Boolean

Returns:

  • (Boolean)


24
25
26
27
28
29
30
31
32
# File 'lib/crash_watch/gdb_controller.rb', line 24

def self.gdb_installed?
	ENV['PATH'].to_s.split(/:+/).each do |path|
		filename = "#{path}/gdb"
		if File.file?(filename) && File.executable?(filename)
			return true
		end
	end
	return false
end

Instance Method Details

#all_threads_backtracesObject



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

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

#attach(pid) ⇒ Object

Raises:

  • (ArgumentError)


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

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



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

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

#closeObject



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

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



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

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

#closed?Boolean

Returns:

  • (Boolean)


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

def closed?
	return !@pid
end

#current_threadObject



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

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

#current_thread_backtraceObject



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

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

#execute(command_string, timeout = nil) ⇒ Object



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

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



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

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

#ruby_backtraceObject



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

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



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

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