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