#!/usr/bin/python
"""
Simple crash handling application for autotest
@copyright Red Hat Inc 2009
@author Lucas Meneghel Rodrigues <lmr@redhat.com>
"""
import sys, os, commands, glob, shutil, syslog, re, time, random, string
def generate_random_string(length):
"""
Return a random string using alphanumeric characters.
@length: length of the string that will be generated.
"""
r = random.SystemRandom()
str = ""
chars = string.letters + string.digits
while length > 0:
str += r.choice(chars)
length -= 1
return str
def get_parent_pid(pid):
"""
Returns the parent PID for a given PID, converted to an integer.
@param pid: Process ID.
"""
try:
ppid = int(open('/proc/%s/stat' % pid).read().split()[3])
except:
# It is not possible to determine the parent because the process
# already left the process table.
ppid = 1
return ppid
def write_to_file(filename, data, report=False):
"""
Write contents to a given file path specified. If not specified, the file
will be created.
@param file_path: Path to a given file.
@param data: File contents.
@param report: Whether we'll use GDB to get a backtrace report of the
file.
"""
f = open(filename, 'w')
try:
f.write(data)
finally:
f.close()
if report:
gdb_report(filename)
return filename
def get_results_dir_list(pid, core_dir_basename):
"""
Get all valid output directories for the core file and the report. It works
by inspecting files created by each test on /tmp and verifying if the
PID of the process that crashed is a child or grandchild of the autotest
test process. If it can't find any relationship (maybe a daemon that died
during a test execution), it will write the core file to the debug dirs
of all tests currently being executed. If there are no active autotest
tests at a particular moment, it will return a list with ['/tmp'].
@param pid: PID for the process that generated the core
@param core_dir_basename: Basename for the directory that will hold both
the core dump and the crash report.
"""
pid_dir_dict = {}
for debugdir_file in glob.glob("/tmp/autotest_results_dir.*"):
a_pid = os.path.splitext(debugdir_file)[1]
results_dir = open(debugdir_file).read().strip()
pid_dir_dict[a_pid] = os.path.join(results_dir, core_dir_basename)
results_dir_list = []
# If a bug occurs and we can't grab the PID for the process that died, just
# return all directories available and write to all of them.
if pid is not None:
while pid > 1:
if pid in pid_dir_dict:
results_dir_list.append(pid_dir_dict[pid])
pid = get_parent_pid(pid)
else:
results_dir_list = pid_dir_dict.values()
return (results_dir_list or
pid_dir_dict.values() or
[os.path.join("/tmp", core_dir_basename)])
def get_info_from_core(path):
"""
Reads a core file and extracts a dictionary with useful core information.
Right now, the only information extracted is the full executable name.
@param path: Path to core file.
"""
full_exe_path = None
output = commands.getoutput('gdb -c %s batch' % path)
path_pattern = re.compile("Core was generated by `([^\0]+)'", re.IGNORECASE)
match = re.findall(path_pattern, output)
for m in match:
# Sometimes the command line args come with the core, so get rid of them
m = m.split(" ")[0]
if os.path.isfile(m):
full_exe_path = m
break
if full_exe_path is None:
syslog.syslog("Could not determine from which application core file %s "
"is from" % path)
return {'full_exe_path': full_exe_path}
def gdb_report(path):
"""
Use GDB to produce a report with information about a given core.
@param path: Path to core file.
"""
# Get full command path
exe_path = get_info_from_core(path)['full_exe_path']
basedir = os.path.dirname(path)
gdb_command_path = os.path.join(basedir, 'gdb_cmd')
if exe_path is not None:
# Write a command file for GDB
gdb_command = 'bt full\n'
write_to_file(gdb_command_path, gdb_command)
# Take a backtrace from the running program
gdb_cmd = ('gdb -e %s -c %s -x %s -n -batch -quiet' %
(exe_path, path, gdb_command_path))
backtrace = commands.getoutput(gdb_cmd)
# Sanitize output before passing it to the report
backtrace = backtrace.decode('utf-8', 'ignore')
else:
exe_path = "Unknown"
backtrace = ("Could not determine backtrace for core file %s" % path)
# Composing the format_dict
report = "Program: %s\n" % exe_path
if crashed_pid is not None:
report += "PID: %s\n" % crashed_pid
if signal is not None:
report += "Signal: %s\n" % signal
if hostname is not None:
report += "Hostname: %s\n" % hostname
if crash_time is not None:
report += ("Time of the crash (according to kernel): %s\n" %
time.ctime(float(crash_time)))
report += "Program backtrace:\n%s\n" % backtrace
report_path = os.path.join(basedir, 'report')
write_to_file(report_path, report)
def write_cores(core_data, dir_list):
"""
Write core files to all directories, optionally providing reports.
@param core_data: Contents of the core file.
@param dir_list: List of directories the cores have to be written.
@param report: Whether reports are to be generated for those core files.
"""
syslog.syslog("Writing core files to %s" % dir_list)
for result_dir in dir_list:
if not os.path.isdir(result_dir):
os.makedirs(result_dir)
core_path = os.path.join(result_dir, 'core')
core_path = write_to_file(core_path, core_file, report=True)
if __name__ == "__main__":
syslog.openlog('AutotestCrashHandler', 0, syslog.LOG_DAEMON)
global crashed_pid, crash_time, uid, signal, hostname, exe
try:
full_functionality = False
try:
crashed_pid, crash_time, uid, signal, hostname, exe = sys.argv[1:]
full_functionality = True
except ValueError, e:
# Probably due a kernel bug, we can't exactly map the parameters
# passed to this script. So we have to reduce the functionality
# of the script (just write the core at a fixed place).
syslog.syslog("Unable to unpack parameters passed to the "
"script. Operating with limited functionality.")
crashed_pid, crash_time, uid, signal, hostname, exe = (None, None,
None, None,
None, None)
if full_functionality:
core_dir_name = 'crash.%s.%s' % (exe, crashed_pid)
else:
core_dir_name = 'core.%s' % generate_random_string(4)
# Get the filtered results dir list
results_dir_list = get_results_dir_list(crashed_pid, core_dir_name)
# Write the core file to the appropriate directory
# (we are piping it to this script)
core_file = sys.stdin.read()
if (exe is not None) and (crashed_pid is not None):
syslog.syslog("Application %s, PID %s crashed" % (exe, crashed_pid))
write_cores(core_file, results_dir_list)
except Exception, e:
syslog.syslog("Crash handler had a problem: %s" % e)