crash-watch 1.1.12 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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