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