Last active
December 16, 2015 07:39
-
-
Save benweint/5399911 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# encoding: utf-8 | |
# This file is distributed under New Relic's license terms. | |
# See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details. | |
require 'tempfile' | |
require 'rbconfig' | |
def fail(msg, opts={}) | |
$stderr.puts(msg) | |
usage() if opts[:usage] | |
exit(-1) | |
end | |
def usage | |
$stderr.puts("Usage: #{$0} <target_pid>") | |
end | |
class Logger | |
def self.log(msg) | |
@messages ||= [] | |
@messages << [Time.now, msg] | |
end | |
def self.messages | |
@messages | |
end | |
end | |
class ShellWrapper | |
def self.execute(cmd) | |
Logger.log("Executing '#{cmd}'") | |
`#{cmd} 2>&1` | |
end | |
end | |
class ProcessDataProvider | |
attr_reader :pid | |
def initialize(pid) | |
@pid = pid | |
end | |
def attachable? | |
begin | |
my_uid = Process.uid | |
(my_uid == 0 || uid == my_uid) | |
rescue | |
return false | |
end | |
end | |
def alive? | |
Process.kill(0, @pid.to_i) | |
return true | |
rescue Errno::ESRCH | |
return false | |
end | |
def uid | |
ShellWrapper.execute("ps -o uid #{pid}").split("\n").last.strip.to_i | |
end | |
def user | |
ShellWrapper.execute("ps -o user #{pid}").split("\n").last.strip | |
end | |
def ppid | |
ShellWrapper.execute("ps -o ppid #{pid}").split("\n").last | |
end | |
def rss | |
ShellWrapper.execute("ps -o rss #{pid}").split("\n").last.to_i | |
end | |
def cpu | |
ShellWrapper.execute("ps -o cpu #{pid}").split("\n").last | |
end | |
def open_files | |
ShellWrapper.execute("lsof -p #{pid}") | |
end | |
def self.for_process(pid) | |
case RbConfig::CONFIG['target_os'] | |
when /linux/ then LinuxProcessDataProvider.new(pid) | |
when /darwin/ then DarwinProcessDataProvider.new(pid) | |
end | |
end | |
end | |
class LinuxProcessDataProvider < ProcessDataProvider | |
def proc_path(item) | |
File.join("/proc/#{pid}", item) | |
end | |
def procline | |
File.read(proc_path('cmdline')).gsub("\000", " ") | |
end | |
def environment | |
File.read(proc_path('environ')).gsub("\000", "\n") | |
end | |
end | |
class DarwinProcessDataProvider < ProcessDataProvider | |
def procline | |
ShellWrapper.execute("ps -o command #{pid}").split("\n").last | |
end | |
def environment | |
cmd = "ps -o command -E #{pid}" | |
ShellWrapper.execute(cmd).split("\n").last.gsub(procline, '').strip | |
end | |
end | |
class RubyProcess | |
attr_accessor :pid | |
def initialize(pid) | |
@pid = pid | |
@provider = ProcessDataProvider.for_process(pid) | |
end | |
[:uid, :ppid, :rss, :cpu, :open_files, :procline, :environment, :alive?, :attachable?].each do |m| | |
define_method(m) do | |
@provider.send(m) | |
end | |
end | |
def gather_backtraces | |
backtrace_file = Tempfile.new('nrdebug_ruby_bt') | |
File.chmod(0666, backtrace_file.path) | |
gdb_script_body = <<-END | |
attach #{pid} | |
t a a bt | |
call (void)close(1) | |
call (void)close(2) | |
call (int)open("#{backtrace_file.path}", 2, 0) | |
call (int)open("#{backtrace_file.path}", 2, 0) | |
call (void)rb_backtrace() | |
call (void)fflush(0) | |
quit | |
END | |
Logger.log("Using gdb script:\n#{gdb_script_body}") | |
script_file = Tempfile.new('nrdebug_gdb_script') | |
script_file.write(gdb_script_body) | |
script_file.close | |
gdb_stderr = Tempfile.new('nrdebug_gdb_stderr') | |
gdb_cmd = "gdb -batch -x #{script_file.path} 2>#{gdb_stderr.path}" | |
gdb_output = ShellWrapper.execute(gdb_cmd) | |
ruby_backtrace = File.read(backtrace_file.path) | |
script_file.close! | |
backtrace_file.close! | |
gdb_stderr.close! | |
[gdb_output, ruby_backtrace] | |
end | |
end | |
class ProcessReport | |
attr_reader :target, :path | |
def initialize(target, path=nil) | |
@target = target | |
@path = path | |
end | |
def open | |
if @path | |
File.open(@path, "w") do |f| | |
yield f | |
end | |
else | |
yield $stdout | |
end | |
end | |
def section(f, name=nil) | |
content = begin | |
yield | |
rescue StandardError => e | |
"<Error: #{e}>, backtrace =\n#{e.backtrace.join("\n")}" | |
end | |
if name | |
f.puts("#{name}:") | |
f.puts(content) | |
f.puts '' | |
end | |
end | |
def generate | |
open do |f| | |
section(f, "Time") { Time.now } | |
section(f, "PID") { @target.pid } | |
section(f, "Command") { @target.procline } | |
section(f, "RSS") { @target.rss } | |
section(f, "CPU") { @target.cpu } | |
section(f, "Parent PID") { @target.ppid } | |
section(f, "OS") { ShellWrapper.execute('uname -a') } | |
section(f, "Environment") { @target.environment } | |
section(f) do | |
c_backtraces, ruby_backtraces = @target.gather_backtraces | |
if c_backtraces.match(/could not attach/i) | |
fail("Failed to attach to target process. Please try again with sudo.") | |
end | |
section(f, "C Backtraces") { c_backtraces } | |
section(f, "Ruby Backtrace") { ruby_backtraces } | |
end | |
section(f, "Open files") { @target.open_files } | |
section(f, "Log") do | |
commands = Logger.messages.map { |(_,msg)| msg } | |
commands.join("\n") | |
end | |
end | |
end | |
end | |
def prompt_for_confirmation(target_pid, target_cmd) | |
puts "Are you sure you want to attach to PID #{target_pid} ('#{target_cmd}')}?" | |
puts '' | |
puts "Extracting debug information from this process may cause it to hang, crash," | |
puts "or otherwise malfunction. It is highly recommended that you only run this" | |
puts "script against processes that are already unresponsive." | |
puts '' | |
puts "Additionally, the output may contain sensitive information from the program's" | |
puts "command line arguments, environment, or open file list. Please examine the" | |
puts "output before sharing it." | |
puts '' | |
puts "To continue, type 'continue':" | |
until ($stdin.gets.strip == 'continue') do | |
puts "Please type 'continue' to continue, or ctrl-c to abort." | |
end | |
end | |
target_pid = ARGV[0] | |
fail("Please provide a PID for the target process", :usage => true) unless target_pid | |
gdb_path = `which gdb` | |
fail("Could not find gdb, please ensure it is installed and in your PATH") if gdb_path.empty? | |
target = RubyProcess.new(target_pid) | |
if !target.attachable? | |
fail("You do not appear to have permissions to attach to the target process.\nPlease check the process owner and try again with sudo if necessary") | |
end | |
if !target.alive? | |
fail("Could not find process with PID #{target_pid}") | |
end | |
target_cmd = target.procline | |
prompt_for_confirmation(target_pid, target_cmd) | |
puts '' | |
puts "Attaching to PID #{target_pid} ('#{target_cmd}')" | |
timestamp = Time.now.to_i | |
report_filename = "nrdebug-#{target_pid}-#{timestamp}.log" | |
report = ProcessReport.new(target, report_filename) | |
report.generate | |
puts "Generated '#{report_filename}'" | |
puts '' | |
puts "Please examine the output file for potentially sensitive information and" | |
puts "remove it before sharing this file with anyone." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment