crash-watch 1.1.12 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,88 +1,25 @@
1
1
  #!/usr/bin/env ruby
2
- # encoding: binary
3
- require 'optparse'
2
+ # Copyright (c) 2010-2016 Phusion Holding B.V.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4
22
 
5
- options = {}
6
- parser = OptionParser.new do |opts|
7
- opts.banner = "Usage: crash-watch [options] PID"
8
- opts.separator ""
9
-
10
- opts.separator "Options:"
11
- opts.on("-d", "--debug", "Show GDB commands that crash-watch sends.") do
12
- options[:debug] = true
13
- end
14
- opts.on("--dump", "Dump current process backtrace and exit immediately.") do
15
- options[:dump] = true
16
- end
17
- opts.on("-v", "--version", "Show version number.") do
18
- options[:version] = true
19
- end
20
- opts.on("-h", "--help", "Show this help message.") do
21
- options[:help] = true
22
- end
23
- end
24
- begin
25
- parser.parse!
26
- rescue OptionParser::ParseError => e
27
- puts e
28
- puts
29
- puts "Please see '--help' for valid options."
30
- exit 1
31
- end
32
-
33
- if options[:help]
34
- puts parser
35
- exit
36
- elsif options[:version]
37
- require 'crash_watch/version'
38
- puts "crash-watch version #{CrashWatch::VERSION_STRING}"
39
- exit
40
- elsif ARGV.size != 1
41
- puts parser
42
- exit 1
43
- end
44
-
45
- $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
46
- require 'crash_watch/gdb_controller'
47
- begin
48
- gdb = CrashWatch::GdbController.new
49
- rescue CrashWatch::Error => e
50
- abort e.message
51
- end
52
- begin
53
- gdb.debug = options[:debug]
54
-
55
- # Ruby sometimes uses SIGVTARLM for thread scheduling.
56
- gdb.execute("handle SIGVTALRM noprint pass")
57
-
58
- if gdb.attach(ARGV[0])
59
- if options[:dump]
60
- puts "Current thread (#{gdb.current_thread}) backtrace:"
61
- puts " " << gdb.current_thread_backtrace.gsub(/\n/, "\n ")
62
- puts
63
- puts "All thread backtraces:"
64
- puts " " << gdb.all_threads_backtraces.gsub(/\n/, "\n ")
65
- else
66
- puts "Monitoring PID #{ARGV[0]}..."
67
- exit_info = gdb.wait_until_exit
68
- puts "Process exited at #{Time.now}."
69
- puts "Exit code: #{exit_info.exit_code}" if exit_info.exit_code
70
- puts "Signal: #{exit_info.signal}" if exit_info.signaled?
71
- if exit_info.backtrace
72
- puts "Backtrace:"
73
- puts " " << exit_info.backtrace.gsub(/\n/, "\n ")
74
- end
75
- end
76
- else
77
- puts "ERROR: Cannot attach to process."
78
- if File.exist?("/proc/sys/kernel/yama/ptrace_scope")
79
- puts
80
- puts "This may be the result of kernel ptrace() hardening. Try disabling it with:"
81
- puts " sudo sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'"
82
- puts "See http://askubuntu.com/questions/41629/after-upgrade-gdb-wont-attach-to-process for more information."
83
- end
84
- exit 2
85
- end
86
- ensure
87
- gdb.close
88
- end
23
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
24
+ require 'crash_watch/app'
25
+ CrashWatch::App.new.run(ARGV)
@@ -2,18 +2,16 @@ require File.expand_path('lib/crash_watch/version', File.dirname(__FILE__))
2
2
  require File.expand_path('lib/crash_watch/packaging', File.dirname(__FILE__))
3
3
 
4
4
  Gem::Specification.new do |s|
5
- s.name = "crash-watch"
6
- s.version = CrashWatch::VERSION_STRING
7
- s.authors = ["Hongli Lai"]
8
- s.description = "Monitor processes and display useful information when they crash."
9
- s.summary = "Monitor processes and display useful information when they crash"
10
- s.email = "software-signing@phusion.nl"
11
- s.files = Dir[*CRASH_WATCH_FILES]
12
- s.homepage = "https://github.com/FooBarWidget/crash-watch"
13
- s.rdoc_options = ["--charset=UTF-8"]
14
- s.executables = ["crash-watch"]
15
- s.require_paths = ["lib"]
16
- s.add_development_dependency("ffi")
17
- s.add_development_dependency("rspec")
5
+ s.name = "crash-watch"
6
+ s.version = CrashWatch::VERSION_STRING
7
+ s.authors = ["Hongli Lai"]
8
+ s.description = "Monitor processes and display useful information when they crash."
9
+ s.summary = "Monitor processes and display useful information when they crash"
10
+ s.email = "software-signing@phusion.nl"
11
+ s.files = Dir[*CRASH_WATCH_FILES]
12
+ s.homepage = "https://github.com/FooBarWidget/crash-watch"
13
+ s.rdoc_options = ["--charset=UTF-8"]
14
+ s.executables = ["crash-watch"]
15
+ s.require_paths = ["lib"]
18
16
  end
19
17
 
@@ -0,0 +1,138 @@
1
+ # encoding: binary
2
+ #
3
+ # Copyright (c) 2010-2016 Phusion Holding B.V.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'optparse'
25
+ require 'crash_watch/gdb_controller'
26
+ require 'crash_watch/lldb_controller'
27
+ require 'crash_watch/version'
28
+
29
+ module CrashWatch
30
+ class App
31
+ def run(argv)
32
+ options = {}
33
+ parser = OptionParser.new do |opts|
34
+ opts.banner = "Usage: crash-watch [options] PID"
35
+ opts.separator ""
36
+
37
+ opts.separator "Options:"
38
+ opts.on("-d", "--debug", "Show GDB commands that crash-watch sends.") do
39
+ options[:debug] = true
40
+ end
41
+ opts.on("--dump", "Dump current process backtrace and exit immediately.") do
42
+ options[:dump] = true
43
+ end
44
+ opts.on("-v", "--version", "Show version number.") do
45
+ options[:version] = true
46
+ end
47
+ opts.on("-h", "--help", "Show this help message.") do
48
+ options[:help] = true
49
+ end
50
+ end
51
+ begin
52
+ parser.parse!(argv)
53
+ rescue OptionParser::ParseError => e
54
+ puts e
55
+ puts
56
+ puts "Please see '--help' for valid options."
57
+ exit 1
58
+ end
59
+
60
+ if options[:help]
61
+ puts parser
62
+ exit
63
+ elsif options[:version]
64
+ puts "crash-watch version #{CrashWatch::VERSION_STRING}"
65
+ exit
66
+ elsif argv.size != 1
67
+ puts parser
68
+ exit 1
69
+ end
70
+
71
+ begin
72
+ if !CrashWatch::Utils.gdb_installed? && CrashWatch::Utils.lldb_installed?
73
+ gdb = CrashWatch::LldbController.new
74
+ else
75
+ gdb = CrashWatch::GdbController.new
76
+ end
77
+ rescue CrashWatch::Error => e
78
+ abort e.message
79
+ end
80
+ begin
81
+ gdb.debug = options[:debug]
82
+
83
+ # Ruby sometimes uses SIGVTARLM for thread scheduling.
84
+ gdb.execute("handle SIGVTALRM noprint pass")
85
+
86
+ if gdb.attach(argv[0])
87
+ if options[:dump]
88
+ puts "*******************************************************"
89
+ puts "*"
90
+ puts "* Current thread (#{gdb.current_thread}) backtrace"
91
+ puts "*"
92
+ puts "*******************************************************"
93
+ puts
94
+ puts " " << gdb.current_thread_backtrace.gsub(/\n/, "\n ")
95
+ puts
96
+ puts
97
+ puts "*******************************************************"
98
+ puts "*"
99
+ puts "* All thread backtraces"
100
+ puts "*"
101
+ puts "*******************************************************"
102
+ puts
103
+ output = gdb.all_threads_backtraces
104
+ output.gsub!(/\n/, "\n ")
105
+ output.insert(0, " ")
106
+ output.gsub!(/^ (Thread .*):$/, "########### \\1 ###########\n")
107
+ puts output
108
+ else
109
+ if gdb.respond_to?(:wait_until_exit)
110
+ puts "Monitoring PID #{argv[0]}..."
111
+ exit_info = gdb.wait_until_exit
112
+ puts "Process exited at #{Time.now}."
113
+ puts "Exit code: #{exit_info.exit_code}" if exit_info.exit_code
114
+ puts "Signal: #{exit_info.signal}" if exit_info.signaled?
115
+ if exit_info.backtrace
116
+ puts "Backtrace:"
117
+ puts " " << exit_info.backtrace.gsub(/\n/, "\n ")
118
+ end
119
+ else
120
+ abort "ERROR: monitoring not supported with LLDB"
121
+ end
122
+ end
123
+ else
124
+ puts "ERROR: Cannot attach to process."
125
+ if File.exist?("/proc/sys/kernel/yama/ptrace_scope")
126
+ puts
127
+ puts "This may be the result of kernel ptrace() hardening. Try disabling it with:"
128
+ puts " sudo sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'"
129
+ puts "See http://askubuntu.com/questions/41629/after-upgrade-gdb-wont-attach-to-process for more information."
130
+ end
131
+ exit 2
132
+ end
133
+ ensure
134
+ gdb.close
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright (c) 2016 Phusion Holding B.V.
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module CrashWatch
23
+ class Error < StandardError
24
+ end
25
+ end
@@ -1,321 +1,277 @@
1
1
  # encoding: binary
2
+ #
3
+ # Copyright (c) 2010-2016 Phusion Holding B.V.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
2
24
  require 'rbconfig'
25
+ require 'crash_watch/base'
26
+ require 'crash_watch/utils'
3
27
 
4
28
  module CrashWatch
29
+ class GdbNotFound < Error
30
+ end
5
31
 
6
- class Error < StandardError
7
- end
32
+ class GdbBroken < Error
33
+ end
8
34
 
9
- class GdbNotFound < Error
10
- end
35
+ class GdbController
36
+ class ExitInfo
37
+ attr_reader :exit_code, :signal, :backtrace, :snapshot
11
38
 
12
- class GdbBroken < Error
13
- end
39
+ def initialize(exit_code, signal, backtrace, snapshot)
40
+ @exit_code = exit_code
41
+ @signal = signal
42
+ @backtrace = backtrace
43
+ @snapshot = snapshot
44
+ end
14
45
 
15
- class GdbController
16
- class ExitInfo
17
- attr_reader :exit_code, :signal, :backtrace, :snapshot
18
-
19
- def initialize(exit_code, signal, backtrace, snapshot)
20
- @exit_code = exit_code
21
- @signal = signal
22
- @backtrace = backtrace
23
- @snapshot = snapshot
24
- end
25
-
26
- def signaled?
27
- !!@signal
28
- end
29
- end
30
-
31
- END_OF_RESPONSE_MARKER = '--------END_OF_RESPONSE--------'
32
-
33
- attr_accessor :debug
46
+ def signaled?
47
+ !!@signal
48
+ end
49
+ end
34
50
 
35
- def initialize
36
- @pid, @in, @out = popen_command(find_gdb, "-n", "-q")
37
- execute("set prompt ")
38
- end
39
-
40
- def execute(command_string, timeout = nil)
41
- raise "GDB session is already closed" if !@pid
42
- puts "gdb write #{command_string.inspect}" if @debug
43
- @in.puts(command_string)
44
- @in.puts("echo \\n#{END_OF_RESPONSE_MARKER}\\n")
45
- done = false
46
- result = ""
47
- while !done
48
- begin
49
- if select([@out], nil, nil, timeout)
50
- line = @out.readline
51
- puts "gdb read #{line.inspect}" if @debug
52
- if line == "#{END_OF_RESPONSE_MARKER}\n"
53
- done = true
54
- else
55
- result << line
56
- end
57
- else
58
- close!
59
- done = true
60
- result = nil
61
- end
62
- rescue EOFError
63
- done = true
64
- end
65
- end
66
- return result
67
- end
68
-
69
- def closed?
70
- return !@pid
71
- end
72
-
73
- def close
74
- if !closed?
75
- begin
76
- execute("detach", 5)
77
- execute("quit", 5) if !closed?
78
- rescue Errno::EPIPE
79
- end
80
- if !closed?
81
- @in.close
82
- @out.close
83
- Process.waitpid(@pid)
84
- @pid = nil
85
- end
86
- end
87
- end
88
-
89
- def close!
90
- if !closed?
91
- @in.close
92
- @out.close
93
- Process.kill('KILL', @pid)
94
- Process.waitpid(@pid)
95
- @pid = nil
96
- end
97
- end
98
-
99
- def attach(pid)
100
- pid = pid.to_s.strip
101
- raise ArgumentError if pid.empty?
102
- result = execute("attach #{pid}")
103
- return result !~ /(No such process|Unable to access task|Operation not permitted)/
104
- end
105
-
106
- def call(code)
107
- result = execute("call #{code}")
108
- result =~ /= (.*)$/
109
- return $1
110
- end
111
-
112
- def program_counter
113
- return execute("p/x $pc").gsub(/.* = /, '')
114
- end
115
-
116
- def current_thread
117
- execute("thread") =~ /Current thread is (.+?) /
118
- return $1
119
- end
120
-
121
- def current_thread_backtrace
122
- return execute("bt full").strip
123
- end
124
-
125
- def all_threads_backtraces
126
- return execute("thread apply all bt full").strip
127
- end
128
-
129
- def ruby_backtrace
130
- filename = "/tmp/gdb-capture.#{@pid}.txt"
131
-
132
- orig_stdout_fd_copy = call("(int) dup(1)")
133
- new_stdout = call(%Q{(void *) fopen("#{filename}", "w")})
134
- new_stdout_fd = call("(int) fileno(#{new_stdout})")
135
- call("(int) dup2(#{new_stdout_fd}, 1)")
136
-
137
- # Let's hope stdout is set to line buffered or unbuffered mode...
138
- call("(void) rb_backtrace()")
139
-
140
- call("(int) dup2(#{orig_stdout_fd_copy}, 1)")
141
- call("(int) fclose(#{new_stdout})")
142
- call("(int) close(#{orig_stdout_fd_copy})")
143
-
144
- if File.exist?(filename)
145
- result = File.read(filename)
146
- result.strip!
147
- if result.empty?
148
- return nil
149
- else
150
- return result
151
- end
152
- else
153
- return nil
154
- end
155
- ensure
156
- if filename
157
- File.unlink(filename) rescue nil
158
- end
159
- end
160
-
161
- def wait_until_exit
162
- execute("break _exit")
163
-
164
- signal = nil
165
- backtraces = nil
166
- snapshot = nil
167
-
168
- while true
169
- result = execute("continue")
170
- if result =~ /^Program received signal (.+?),/
171
- signal = $1
172
- backtraces = execute("thread apply all bt full").strip
173
- if backtraces.empty?
174
- backtraces = execute("bt full").strip
175
- end
176
- snapshot = yield(self) if block_given?
177
-
178
- # This signal may or may not be immediately fatal; the
179
- # signal might be ignored by the process, or the process
180
- # has some clever signal handler that fixes the state,
181
- # or maybe the signal handler must run some cleanup code
182
- # before killing the process. Let's find out by running
183
- # the next machine instruction.
184
- old_program_counter = program_counter
185
- result = execute("stepi")
186
- if result =~ /^Program received signal .+?,/
187
- # Yes, it was fatal. Here we don't care whether the
188
- # instruction caused a different signal. The last
189
- # one is probably what we're interested in.
190
- return ExitInfo.new(nil, signal, backtraces, snapshot)
191
- elsif result =~ /^Program (terminated|exited)/ || result =~ /^Breakpoint .*? _exit/
192
- # Running the next instruction causes the program to terminate.
193
- # Not sure what's going on but the previous signal and
194
- # backtrace is probably what we're interested in.
195
- return ExitInfo.new(nil, signal, backtraces, snapshot)
196
- elsif old_program_counter == program_counter
197
- # The process cannot continue but we're not sure what GDB
198
- # is telling us.
199
- raise "Unexpected GDB output: #{result}"
200
- end
201
- # else:
202
- # The signal doesn't isn't immediately fatal, so save current
203
- # status, continue, and check whether the process exits later.
204
- elsif result =~ /^Program terminated with signal (.+?),/
205
- if $1 == signal
206
- # Looks like the signal we trapped earlier
207
- # caused an exit.
208
- return ExitInfo.new(nil, signal, backtraces, snapshot)
209
- else
210
- return ExitInfo.new(nil, signal, nil, snapshot)
211
- end
212
- elsif result =~ /^Breakpoint .*? _exit /
213
- backtraces = execute("thread apply all bt full").strip
214
- if backtraces.empty?
215
- backtraces = execute("bt full").strip
216
- end
217
- snapshot = yield(self) if block_given?
218
- # On OS X, gdb may fail to return from the 'continue' command
219
- # even though the process exited. Kernel bug? In any case,
220
- # we put a timeout here so that we don't wait indefinitely.
221
- result = execute("continue", 10)
222
- if result =~ /^Program exited with code (\d+)\.$/
223
- return ExitInfo.new($1.to_i, nil, backtraces, snapshot)
224
- elsif result =~ /^Program exited normally/
225
- return ExitInfo.new(0, nil, backtraces, snapshot)
226
- else
227
- return ExitInfo.new(nil, nil, backtraces, snapshot)
228
- end
229
- elsif result =~ /^Program exited with code (\d+)\.$/
230
- return ExitInfo.new($1.to_i, nil, nil, nil)
231
- elsif result =~ /^Program exited normally/
232
- return ExitInfo.new(0, nil, nil, nil)
233
- else
234
- return ExitInfo.new(nil, nil, nil, nil)
235
- end
236
- end
237
- end
238
-
239
- private
240
- def popen_command(*command)
241
- a, b = IO.pipe
242
- c, d = IO.pipe
243
- if Process.respond_to?(:spawn)
244
- args = command.dup
245
- args << {
246
- STDIN => a,
247
- STDOUT => d,
248
- STDERR => d,
249
- :close_others => true
250
- }
251
- pid = Process.spawn(*args)
252
- else
253
- pid = fork do
254
- STDIN.reopen(a)
255
- STDOUT.reopen(d)
256
- STDERR.reopen(d)
257
- b.close
258
- c.close
259
- exec(*command)
260
- end
261
- end
262
- a.close
263
- d.close
264
- b.binmode
265
- c.binmode
266
- return [pid, b, c]
267
- end
51
+ END_OF_RESPONSE_MARKER = '--------END_OF_RESPONSE--------'
268
52
 
269
- def find_gdb
270
- result = nil
271
- if ENV['GDB'] && File.executable?(ENV['GDB'])
272
- result = ENV['GDB']
273
- else
274
- ENV['PATH'].to_s.split(/:+/).each do |path|
275
- filename = "#{path}/gdb"
276
- if File.file?(filename) && File.executable?(filename)
277
- result = filename
278
- break
279
- end
280
- end
281
- end
53
+ attr_accessor :debug
282
54
 
283
- puts "Found gdb at: #{result}" if result
55
+ def initialize
56
+ @pid, @in, @out = Utils.popen_command(find_gdb, "-n", "-q")
57
+ execute("set prompt ")
58
+ end
284
59
 
285
- config = defined?(RbConfig) ? RbConfig::CONFIG : Config::CONFIG
286
- if config['target_os'] =~ /freebsd/ && result == "/usr/bin/gdb"
287
- # /usr/bin/gdb on FreeBSD is broken:
288
- # https://github.com/FooBarWidget/crash-watch/issues/1
289
- # Look for a newer one that's installed from ports.
290
- puts "#{result} is broken on FreeBSD. Looking for an alternative..."
291
- result = nil
292
- ["/usr/local/bin/gdb76", "/usr/local/bin/gdb66"].each do |candidate|
293
- if File.executable?(candidate)
294
- result = candidate
295
- break
296
- end
297
- end
60
+ def execute(command_string, timeout = nil)
61
+ raise "GDB session is already closed" if !@pid
62
+ puts "gdb write #{command_string.inspect}" if @debug
63
+ @in.puts(command_string)
64
+ @in.puts("echo \\n#{END_OF_RESPONSE_MARKER}\\n")
65
+ done = false
66
+ result = ""
67
+ while !done
68
+ begin
69
+ if select([@out], nil, nil, timeout)
70
+ line = @out.readline
71
+ puts "gdb read #{line.inspect}" if @debug
72
+ if line == "#{END_OF_RESPONSE_MARKER}\n"
73
+ done = true
74
+ else
75
+ result << line
76
+ end
77
+ else
78
+ close!
79
+ done = true
80
+ result = nil
81
+ end
82
+ rescue EOFError
83
+ done = true
84
+ end
85
+ end
86
+ result
87
+ end
298
88
 
299
- if result.nil?
300
- raise GdbBroken,
301
- "*** ERROR ***: '/usr/bin/gdb' is broken on FreeBSD. " +
302
- "Please install the one from the devel/gdb port instead. " +
303
- "If you want to use another gdb"
304
- else
305
- puts "Found gdb at: #{result}" if result
306
- return result
307
- end
308
- elsif result.nil?
309
- raise GdbNotFound,
310
- "*** ERROR ***: 'gdb' isn't installed. Please install it first.\n" +
311
- " Debian/Ubuntu: sudo apt-get install gdb\n" +
312
- "RedHat/CentOS/Fedora: sudo yum install gdb\n" +
313
- " Mac OS X: please install the Developer Tools or XCode\n" +
314
- " FreeBSD: use the devel/gdb port\n"
315
- else
316
- return result
317
- end
318
- end
319
- end
89
+ def closed?
90
+ !@pid
91
+ end
92
+
93
+ def close
94
+ if !closed?
95
+ begin
96
+ execute("detach", 5)
97
+ execute("quit", 5) if !closed?
98
+ rescue Errno::EPIPE
99
+ end
100
+ if !closed?
101
+ @in.close
102
+ @out.close
103
+ Process.waitpid(@pid)
104
+ @pid = nil
105
+ end
106
+ end
107
+ end
108
+
109
+ def close!
110
+ if !closed?
111
+ @in.close
112
+ @out.close
113
+ Process.kill('KILL', @pid)
114
+ Process.waitpid(@pid)
115
+ @pid = nil
116
+ end
117
+ end
118
+
119
+ def attach(pid)
120
+ pid = pid.to_s.strip
121
+ raise ArgumentError if pid.empty?
122
+ result = execute("attach #{pid}")
123
+ result !~ /(No such process|Unable to access task|Operation not permitted)/
124
+ end
125
+
126
+ def program_counter
127
+ execute("p/x $pc").gsub(/.* = /, '')
128
+ end
129
+
130
+ def current_thread
131
+ execute("thread") =~ /Current thread is (.+?) /
132
+ $1
133
+ end
320
134
 
321
- end
135
+ def current_thread_backtrace
136
+ execute("bt full").strip
137
+ end
138
+
139
+ def all_threads_backtraces
140
+ execute("thread apply all bt full").strip
141
+ end
142
+
143
+ def wait_until_exit
144
+ execute("break _exit")
145
+
146
+ signal = nil
147
+ backtraces = nil
148
+ snapshot = nil
149
+
150
+ while true
151
+ result = execute("continue")
152
+ if result =~ /^(Program|Thread .*) received signal (.+?),/
153
+ signal = $2
154
+ backtraces = execute("thread apply all bt full").strip
155
+ if backtraces.empty?
156
+ backtraces = execute("bt full").strip
157
+ end
158
+ snapshot = yield(self) if block_given?
159
+
160
+ # This signal may or may not be immediately fatal; the
161
+ # signal might be ignored by the process, or the process
162
+ # has some clever signal handler that fixes the state,
163
+ # or maybe the signal handler must run some cleanup code
164
+ # before killing the process. Let's find out by running
165
+ # the next machine instruction.
166
+ old_program_counter = program_counter
167
+ result = execute("stepi")
168
+ if result =~ /^(Program|Thread .*) received signal .+?,/
169
+ # Yes, it was fatal. Here we don't care whether the
170
+ # instruction caused a different signal. The last
171
+ # one is probably what we're interested in.
172
+ return ExitInfo.new(nil, signal, backtraces, snapshot)
173
+ elsif result =~ /^Program (terminated|exited)/ || result =~ /^Breakpoint .*? (__GI_)?_exit/
174
+ # Running the next instruction causes the program to terminate.
175
+ # Not sure what's going on but the previous signal and
176
+ # backtrace is probably what we're interested in.
177
+ return ExitInfo.new(nil, signal, backtraces, snapshot)
178
+ elsif old_program_counter == program_counter
179
+ # The process cannot continue but we're not sure what GDB
180
+ # is telling us.
181
+ raise "Unexpected GDB output: #{result}"
182
+ end
183
+ # else:
184
+ # The signal doesn't isn't immediately fatal, so save current
185
+ # status, continue, and check whether the process exits later.
186
+ elsif result =~ /^Program terminated with signal (.+?),/
187
+ if $1 == signal
188
+ # Looks like the signal we trapped earlier
189
+ # caused an exit.
190
+ return ExitInfo.new(nil, signal, backtraces, snapshot)
191
+ else
192
+ return ExitInfo.new(nil, signal, nil, snapshot)
193
+ end
194
+ elsif result =~ /Breakpoint .*? (__GI_)?_exit (\(status=(\d+)\))?/
195
+ status_param = $3
196
+ backtraces = execute("thread apply all bt full").strip
197
+ if backtraces.empty?
198
+ backtraces = execute("bt full").strip
199
+ end
200
+ snapshot = yield(self) if block_given?
201
+ # On OS X, gdb may fail to return from the 'continue' command
202
+ # even though the process exited. Kernel bug? In any case,
203
+ # we put a timeout here so that we don't wait indefinitely.
204
+ result = execute("continue", 10)
205
+ if result =~ /^(Program|.*process .*) exited with code (\d+)/
206
+ return ExitInfo.new($2.to_i, nil, backtraces, snapshot)
207
+ elsif result =~ /^(Program|.*process .*) exited normally/
208
+ return ExitInfo.new(0, nil, backtraces, snapshot)
209
+ elsif status_param.nil? || status_param.empty?
210
+ return ExitInfo.new(nil, nil, backtraces, snapshot)
211
+ else
212
+ return ExitInfo.new(status_param.to_i, nil, backtraces, snapshot)
213
+ end
214
+ elsif result =~ /^(Program|.*process .*) exited with code (\d+)\.$/
215
+ return ExitInfo.new($2.to_i, nil, nil, nil)
216
+ elsif result =~ /^(Program|.*process .*) exited normally/
217
+ return ExitInfo.new(0, nil, nil, nil)
218
+ else
219
+ return ExitInfo.new(nil, nil, nil, nil)
220
+ end
221
+ end
222
+ end
223
+
224
+ private
225
+ def find_gdb
226
+ result = nil
227
+ if ENV['GDB'] && File.executable?(ENV['GDB'])
228
+ result = ENV['GDB']
229
+ else
230
+ ENV['PATH'].to_s.split(/:+/).each do |path|
231
+ filename = "#{path}/gdb"
232
+ if File.file?(filename) && File.executable?(filename)
233
+ result = filename
234
+ break
235
+ end
236
+ end
237
+ end
238
+
239
+ puts "Found gdb at: #{result}" if result
240
+
241
+ config = defined?(RbConfig) ? RbConfig::CONFIG : Config::CONFIG
242
+ if config['target_os'] =~ /freebsd/ && result == "/usr/bin/gdb"
243
+ # /usr/bin/gdb on FreeBSD is broken:
244
+ # https://github.com/FooBarWidget/crash-watch/issues/1
245
+ # Look for a newer one that's installed from ports.
246
+ puts "#{result} is broken on FreeBSD. Looking for an alternative..."
247
+ result = nil
248
+ ["/usr/local/bin/gdb76", "/usr/local/bin/gdb66"].each do |candidate|
249
+ if File.executable?(candidate)
250
+ result = candidate
251
+ break
252
+ end
253
+ end
254
+
255
+ if result.nil?
256
+ raise GdbBroken,
257
+ "*** ERROR ***: '/usr/bin/gdb' is broken on FreeBSD. " +
258
+ "Please install the one from the devel/gdb port instead. " +
259
+ "If you want to use another gdb"
260
+ else
261
+ puts "Found gdb at: #{result}" if result
262
+ result
263
+ end
264
+ elsif result.nil?
265
+ raise GdbNotFound,
266
+ "*** ERROR ***: 'gdb' not found. Please install it (and if using Nginx " +
267
+ "ensure that PATH isn't filtered out, see also its \"env\" option).\n" +
268
+ " Debian/Ubuntu: sudo apt-get install gdb\n" +
269
+ "RedHat/CentOS/Fedora: sudo yum install gdb\n" +
270
+ " Mac OS X: please install the Developer Tools or XCode\n" +
271
+ " FreeBSD: use the devel/gdb port\n"
272
+ else
273
+ result
274
+ end
275
+ end
276
+ end
277
+ end