1#!/usr/bin/python 2""" 3Simple crash handling application for autotest 4 5@copyright Red Hat Inc 2009 6@author Lucas Meneghel Rodrigues <lmr@redhat.com> 7""" 8import sys, os, commands, glob, shutil, syslog, re, time, random, string 9 10 11def generate_random_string(length): 12 """ 13 Return a random string using alphanumeric characters. 14 15 @length: length of the string that will be generated. 16 """ 17 r = random.SystemRandom() 18 str = "" 19 chars = string.letters + string.digits 20 while length > 0: 21 str += r.choice(chars) 22 length -= 1 23 return str 24 25 26def get_parent_pid(pid): 27 """ 28 Returns the parent PID for a given PID, converted to an integer. 29 30 @param pid: Process ID. 31 """ 32 try: 33 ppid = int(open('/proc/%s/stat' % pid).read().split()[3]) 34 except: 35 # It is not possible to determine the parent because the process 36 # already left the process table. 37 ppid = 1 38 39 return ppid 40 41 42def write_to_file(filename, data, report=False): 43 """ 44 Write contents to a given file path specified. If not specified, the file 45 will be created. 46 47 @param file_path: Path to a given file. 48 @param data: File contents. 49 @param report: Whether we'll use GDB to get a backtrace report of the 50 file. 51 """ 52 f = open(filename, 'w') 53 try: 54 f.write(data) 55 finally: 56 f.close() 57 58 if report: 59 gdb_report(filename) 60 61 return filename 62 63 64def get_results_dir_list(pid, core_dir_basename): 65 """ 66 Get all valid output directories for the core file and the report. It works 67 by inspecting files created by each test on /tmp and verifying if the 68 PID of the process that crashed is a child or grandchild of the autotest 69 test process. If it can't find any relationship (maybe a daemon that died 70 during a test execution), it will write the core file to the debug dirs 71 of all tests currently being executed. If there are no active autotest 72 tests at a particular moment, it will return a list with ['/tmp']. 73 74 @param pid: PID for the process that generated the core 75 @param core_dir_basename: Basename for the directory that will hold both 76 the core dump and the crash report. 77 """ 78 pid_dir_dict = {} 79 for debugdir_file in glob.glob("/tmp/autotest_results_dir.*"): 80 a_pid = os.path.splitext(debugdir_file)[1] 81 results_dir = open(debugdir_file).read().strip() 82 pid_dir_dict[a_pid] = os.path.join(results_dir, core_dir_basename) 83 84 results_dir_list = [] 85 # If a bug occurs and we can't grab the PID for the process that died, just 86 # return all directories available and write to all of them. 87 if pid is not None: 88 while pid > 1: 89 if pid in pid_dir_dict: 90 results_dir_list.append(pid_dir_dict[pid]) 91 pid = get_parent_pid(pid) 92 else: 93 results_dir_list = pid_dir_dict.values() 94 95 return (results_dir_list or 96 pid_dir_dict.values() or 97 [os.path.join("/tmp", core_dir_basename)]) 98 99 100def get_info_from_core(path): 101 """ 102 Reads a core file and extracts a dictionary with useful core information. 103 104 Right now, the only information extracted is the full executable name. 105 106 @param path: Path to core file. 107 """ 108 full_exe_path = None 109 output = commands.getoutput('gdb -c %s batch' % path) 110 path_pattern = re.compile("Core was generated by `([^\0]+)'", re.IGNORECASE) 111 match = re.findall(path_pattern, output) 112 for m in match: 113 # Sometimes the command line args come with the core, so get rid of them 114 m = m.split(" ")[0] 115 if os.path.isfile(m): 116 full_exe_path = m 117 break 118 119 if full_exe_path is None: 120 syslog.syslog("Could not determine from which application core file %s " 121 "is from" % path) 122 123 return {'full_exe_path': full_exe_path} 124 125 126def gdb_report(path): 127 """ 128 Use GDB to produce a report with information about a given core. 129 130 @param path: Path to core file. 131 """ 132 # Get full command path 133 exe_path = get_info_from_core(path)['full_exe_path'] 134 basedir = os.path.dirname(path) 135 gdb_command_path = os.path.join(basedir, 'gdb_cmd') 136 137 if exe_path is not None: 138 # Write a command file for GDB 139 gdb_command = 'bt full\n' 140 write_to_file(gdb_command_path, gdb_command) 141 142 # Take a backtrace from the running program 143 gdb_cmd = ('gdb -e %s -c %s -x %s -n -batch -quiet' % 144 (exe_path, path, gdb_command_path)) 145 backtrace = commands.getoutput(gdb_cmd) 146 # Sanitize output before passing it to the report 147 backtrace = backtrace.decode('utf-8', 'ignore') 148 else: 149 exe_path = "Unknown" 150 backtrace = ("Could not determine backtrace for core file %s" % path) 151 152 # Composing the format_dict 153 report = "Program: %s\n" % exe_path 154 if crashed_pid is not None: 155 report += "PID: %s\n" % crashed_pid 156 if signal is not None: 157 report += "Signal: %s\n" % signal 158 if hostname is not None: 159 report += "Hostname: %s\n" % hostname 160 if crash_time is not None: 161 report += ("Time of the crash (according to kernel): %s\n" % 162 time.ctime(float(crash_time))) 163 report += "Program backtrace:\n%s\n" % backtrace 164 165 report_path = os.path.join(basedir, 'report') 166 write_to_file(report_path, report) 167 168 169def write_cores(core_data, dir_list): 170 """ 171 Write core files to all directories, optionally providing reports. 172 173 @param core_data: Contents of the core file. 174 @param dir_list: List of directories the cores have to be written. 175 @param report: Whether reports are to be generated for those core files. 176 """ 177 syslog.syslog("Writing core files to %s" % dir_list) 178 for result_dir in dir_list: 179 if not os.path.isdir(result_dir): 180 os.makedirs(result_dir) 181 core_path = os.path.join(result_dir, 'core') 182 core_path = write_to_file(core_path, core_file, report=True) 183 184 185if __name__ == "__main__": 186 syslog.openlog('AutotestCrashHandler', 0, syslog.LOG_DAEMON) 187 global crashed_pid, crash_time, uid, signal, hostname, exe 188 try: 189 full_functionality = False 190 try: 191 crashed_pid, crash_time, uid, signal, hostname, exe = sys.argv[1:] 192 full_functionality = True 193 except ValueError, e: 194 # Probably due a kernel bug, we can't exactly map the parameters 195 # passed to this script. So we have to reduce the functionality 196 # of the script (just write the core at a fixed place). 197 syslog.syslog("Unable to unpack parameters passed to the " 198 "script. Operating with limited functionality.") 199 crashed_pid, crash_time, uid, signal, hostname, exe = (None, None, 200 None, None, 201 None, None) 202 203 if full_functionality: 204 core_dir_name = 'crash.%s.%s' % (exe, crashed_pid) 205 else: 206 core_dir_name = 'core.%s' % generate_random_string(4) 207 208 # Get the filtered results dir list 209 results_dir_list = get_results_dir_list(crashed_pid, core_dir_name) 210 211 # Write the core file to the appropriate directory 212 # (we are piping it to this script) 213 core_file = sys.stdin.read() 214 215 if (exe is not None) and (crashed_pid is not None): 216 syslog.syslog("Application %s, PID %s crashed" % (exe, crashed_pid)) 217 write_cores(core_file, results_dir_list) 218 219 except Exception, e: 220 syslog.syslog("Crash handler had a problem: %s" % e) 221