Stackhandler: process win32 stacks
authorJean-Baptiste Kempf <jb@videolan.org>
Fri, 7 Sep 2012 12:44:48 +0000 (14:44 +0200)
committerJean-Baptiste Kempf <jb@videolan.org>
Fri, 7 Sep 2012 12:44:48 +0000 (14:44 +0200)
This script was done by etix, and modified by me.

Yes, this is not the most beautiful ever, but it works. Patches welcome

extras/misc/stackhandler.py [new file with mode: 0755]

diff --git a/extras/misc/stackhandler.py b/extras/misc/stackhandler.py
new file mode 100755 (executable)
index 0000000..ff1a1a4
--- /dev/null
@@ -0,0 +1,314 @@
+#!/usr/bin/python
+#####################################################################
+#             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+#                    Version 2, December 2004
+#
+# Copyright (C) 2011-2012 Ludovic Fauvet <etix@videolan.org>
+#                         Jean-Baptiste Kempf <jb@videolan.org>
+#
+# Everyone is permitted to copy and distribute verbatim or modified
+# copies of this license document, and changing it is allowed as long
+# as the name is changed.
+#
+#            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+#   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+#
+#  0. You just DO WHAT THE FUCK YOU WANT TO.
+#####################################################################
+#
+# This script can be started in two ways:
+# - Without any arguments:
+#   The script will search for stacktrace in the WORKDIR, process
+#   them and dispatch them in their respective subdirectories.
+# - With a stacktrace as only argument:
+#   The script will write the output on stdout and exit immediately
+#   after the stacktrace has been processed.
+#   The input file will stay in place, untouched.
+#
+# NOTE: Due to a bug in the mingw32-binutils > 2.19 the section
+#       .gnu_debuglink in the binary file is trimmed thus preventing
+#       gdb to find the associated symbols. This script will
+#       work around this issue and rerun gdb for each dbg file.
+#
+#####################################################################
+
+VLC_VERSION         = "2.0.3"
+VLC_BIN             = "/home/videolan/vlc/2.0.3/vlc-2.0.3/vlc.exe"
+VLC_BASE_DIR        = "/home/videolan/vlc/2.0.3/vlc-2.0.3/"
+VLC_SYMBOLS_DIR     = "/home/videolan/vlc/2.0.3/symbols-2.0.3/"
+WORKDIR             = "/srv/ftp/crashes-win32"
+FILE_MATCH          = r"^\d{14}$"
+FILE_MAX_SIZE       = 10000
+GDB_CMD             = "gdb --exec=%(VLC_BIN)s --symbols=%(VLC_SYMBOLS_DIR)s%(DBG_FILE)s.dbg --batch -x %(BATCH_FILE)s"
+
+EMAIL_TO            = "bugreporter -- videolan.org"
+EMAIL_FROM          = "crashes@crash.videolan.org"
+EMAIL_SUBJECT       = "[CRASH] New Win32 crash report"
+EMAIL_BODY          = \
+"""
+Dear Bug Squasher,
+
+This crash has been reported automatically and might be incomplete and/or broken.
+Windows version: %(WIN32_VERSION)s
+
+%(STACKTRACE)s
+
+Truly yours,
+a python script.
+"""
+
+import os, sys, re, tempfile
+import string, shlex, subprocess
+import smtplib, datetime, shutil
+import traceback
+from email.mime.text import MIMEText
+
+
+def processFile(filename):
+    print "Processing " + filename
+    global win32_version
+
+    f = open(filename, 'r')
+    # Read (and repair) the input file
+    content = "".join(filter(lambda x: x in string.printable, f.read()))
+    f.close()
+
+    if os.path.getsize(filename) < 10:
+        print("File empty")
+    os.remove(filename)
+        return
+
+    # Check if VLC version match
+    if not isValidVersion(content):
+        print("Invalid VLC version")
+        moveFile(filename, outdated = True)
+        return
+
+    # Get Windows version
+    win32_version = getWinVersion(content) or 'unknown'
+
+    # Map eip <--> library
+    mapping = mapLibraries(content)
+    if not mapping:
+        print("Stacktrace not found")
+    os.remove(filename)
+        return
+
+    # Associate all eip to their respective lib
+    # lib1
+    #     `- 0x6904f020
+    #      - 0x6927d37c
+    # lib2
+    #     `- 0x7e418734
+    #      - 0x7e418816
+    #      - 0x7e42bf15
+    sortedEIP,delta_libs = sortEIP(content,mapping)
+    # Compute the stacktrace using GDB
+    eipmap = findSymbols(sortedEIP)
+    # Generate the body of the email
+    body = genEmailBody(mapping, eipmap, delta_libs)
+    # Send the email
+    sendEmail(body)
+    # Print the output
+    print(body)
+
+    # Finally archive the stacktrace
+    moveFile(filename, outdated = False)
+
+def isValidVersion(content):
+    pattern = re.compile(r"^VLC=%s " % VLC_VERSION, re.MULTILINE)
+    res = pattern.search(content)
+    return True if res else False
+
+def getWinVersion(content):
+    pattern = re.compile(r"^OS=(.*)$", re.MULTILINE)
+    res = pattern.search(content)
+    if res is not None:
+        return res.group(1)
+    return None
+
+def getDiffAddress(content, name):
+    plugin_name_section = content.find(name)
+    if plugin_name_section < 0:
+        return None
+
+    begin_index = content.rfind("\n", 0, plugin_name_section) + 1
+    end_index = content.find("|", begin_index)
+
+    tmp_index = name.rfind('plugins\\')
+    libname = name[tmp_index :].replace("\\", "/")
+    full_path = VLC_BASE_DIR + libname
+
+    if not os.path.isfile(full_path):
+        return None
+
+    cmd = "objdump -p " + full_path + " |grep ImageBase -|cut -f2-"
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).stdout.read().strip()
+
+    diff = int(content[begin_index:end_index], 16) - int(p, 16)
+    return diff
+
+def mapLibraries(content):
+    stacktrace_section = content.find("[stacktrace]")
+    if stacktrace_section < 0:
+        return None
+
+    stacklines = content[stacktrace_section:]
+    stacklines = stacklines.splitlines()
+    pattern = re.compile(r"^([0-9a-fA-F]+)\|(.+)$")
+
+    mapping = []
+    for line in stacklines:
+        m = pattern.match(line)
+        print(line)
+        if m is not None:
+            mapping.append(m.group(1, 2))
+
+    if len(mapping) == 0:
+        return None
+    return mapping
+
+
+def sortEIP(content, mapping):
+    # Merge all EIP mapping to the same library
+    libs = {}
+    libs_address = {}
+    for item in mapping:
+        # Extract the library name (without the full path)
+        index = item[1].rfind('\\')
+        libname = item[1][index + 1:]
+
+        # Append the eip to its respective lib
+        if libname not in libs:
+            libs[libname] = []
+            diff = getDiffAddress(content, item[1])
+            if diff is not None:
+                libs_address[libname] = diff
+            else:
+                libs_address[libname] = 0
+
+        libs[libname].append(int(item[0],16) - libs_address[libname])
+
+    return libs,libs_address
+
+
+def findSymbols(sortedEIP):
+    eipmap = {}
+
+    for k, v in sortedEIP.items():
+        # Create the gdb batchfile
+        batchfile = tempfile.NamedTemporaryFile(mode="w")
+        batchfile.write("set print symbol-filename on\n")
+
+        # Append all eip for this lib
+        for eip in v:
+            batchfile.write('p/a %s\n' % hex(eip))
+        batchfile.flush()
+
+        # Generate the command line
+        cmd = GDB_CMD % {"VLC_BIN": VLC_BIN, "VLC_SYMBOLS_DIR": VLC_SYMBOLS_DIR, "DBG_FILE": k, "BATCH_FILE": batchfile.name}
+        args = shlex.split(cmd)
+
+        # Start GDB and get result
+        p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+        # Parse result
+        gdb_pattern = re.compile(r"^\$\d+ = (.+)$")
+        cnt = 0
+        while p.poll() == None:
+            o = p.stdout.readline()
+            if o != b'':
+                o = bytes.decode(o)
+                m = gdb_pattern.match(o)
+                if m is not None:
+                    #print("LINE: [%s]" % m.group(1))
+                    eipmap[v[cnt]] = m.group(1)
+                    cnt += 1
+        batchfile.close()
+    return eipmap
+
+
+def genEmailBody(mapping, eipmap, delta_libs):
+    stacktrace = ""
+    cnt = 0
+    for item in mapping:
+        index = item[1].rfind('\\')
+        libname = item[1][index + 1:]
+        print(int(item[0],16), delta_libs[libname])
+        #print(eipmap)
+        #print(mapping)
+        stacktrace += "%d. %s [in %s]\n" % (cnt, eipmap[int(item[0],16)-delta_libs[libname]], item[1])
+        cnt += 1
+    stacktrace = stacktrace.rstrip('\n')
+    return EMAIL_BODY % {"STACKTRACE": stacktrace, "WIN32_VERSION": win32_version}
+
+
+def sendEmail(body):
+    msg = MIMEText(body)
+    msg['Subject'] = EMAIL_SUBJECT
+    msg['From'] = EMAIL_FROM
+    msg['To'] = EMAIL_TO
+
+    # Send the email
+    s = smtplib.SMTP()
+    s.connect("127.0.0.1")
+    s.sendmail(EMAIL_FROM, [EMAIL_TO], msg.as_string())
+    s.quit()
+
+def moveFile(filename, outdated = False):
+    today = datetime.datetime.now().strftime("%Y%m%d")
+    today_path = "%s/%s" % (WORKDIR, today)
+    if not os.path.isdir(today_path):
+        os.mkdir(today_path)
+    if not outdated:
+        shutil.move(filename, "%s/%s" % (today_path, os.path.basename(filename)))
+    else:
+        outdated_path = "%s/outdated/" % today_path
+        if not os.path.isdir(outdated_path):
+            os.mkdir(outdated_path)
+        shutil.move(filename, "%s/%s" % (outdated_path, os.path.basename(filename)))
+
+
+### ENTRY POINT ###
+
+if len(sys.argv) == 1:
+    print("Folder mode")
+    batch = True
+if len(sys.argv) != 2:
+    print("Running in batch mode")
+    batch = True
+else:
+    batch = False
+
+input_files = []
+if not batch:
+    if not os.path.isfile(sys.argv[1]):
+        exit("file does not exists")
+    input_files.append(sys.argv[1])
+else:
+    file_pattern = re.compile(FILE_MATCH)
+    entries = os.listdir(WORKDIR)
+    for entry in entries:
+        path_entry = WORKDIR + "/" + entry
+        if not os.path.isfile(path_entry):
+            continue
+        if not file_pattern.match(entry):
+            print(entry)
+            os.remove(path_entry)
+            continue
+        if os.path.getsize(path_entry) > FILE_MAX_SIZE:
+            print("%s is too big" % entry)
+            os.remove(path_entry)
+            continue
+        input_files.append(path_entry)
+
+if not len(input_files):
+    exit("Nothing to process")
+
+# Start processing each file
+for input_file in input_files:
+    try:
+        processFile(input_file)
+    except Exception as ex:
+        print(traceback.format_exc())
+