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