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