stackhandler: fix indentation
[vlc.git] / extras / misc / stackhandler.py
1 #!/usr/bin/python
2 #####################################################################
3 #             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
4 #                    Version 2, December 2004
5 #
6 # Copyright (C) 2011-2012 Ludovic Fauvet <etix@videolan.org>
7 #                         Jean-Baptiste Kempf <jb@videolan.org>
8 #
9 # Everyone is permitted to copy and distribute verbatim or modified
10 # copies of this license document, and changing it is allowed as long
11 # as the name is changed.
12 #
13 #            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
14 #   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
15 #
16 #  0. You just DO WHAT THE FUCK YOU WANT TO.
17 #####################################################################
18 #
19 # This script can be started in two ways:
20 # - Without any arguments:
21 #   The script will search for stacktrace in the WORKDIR, process
22 #   them and dispatch them in their respective subdirectories.
23 # - With a stacktrace as only argument:
24 #   The script will write the output on stdout and exit immediately
25 #   after the stacktrace has been processed.
26 #   The input file will stay in place, untouched.
27 #
28 # NOTE: Due to a bug in the mingw32-binutils > 2.19 the section
29 #       .gnu_debuglink in the binary file is trimmed thus preventing
30 #       gdb to find the associated symbols. This script will
31 #       work around this issue and rerun gdb for each dbg file.
32 #
33 #####################################################################
34
35 VLC_VERSION         = "2.0.3"
36 VLC_BIN             = "/home/videolan/vlc/2.0.3/vlc-2.0.3/vlc.exe"
37 VLC_BASE_DIR        = "/home/videolan/vlc/2.0.3/vlc-2.0.3/"
38 VLC_SYMBOLS_DIR     = "/home/videolan/vlc/2.0.3/symbols-2.0.3/"
39 WORKDIR             = "/srv/ftp/crashes-win32"
40 FILE_MATCH          = r"^\d{14}$"
41 FILE_MAX_SIZE       = 10000
42 GDB_CMD             = "gdb --exec=%(VLC_BIN)s --symbols=%(VLC_SYMBOLS_DIR)s%(DBG_FILE)s.dbg --batch -x %(BATCH_FILE)s"
43
44 EMAIL_TO            = "bugreporter -- videolan.org"
45 EMAIL_FROM          = "crashes@crash.videolan.org"
46 EMAIL_SUBJECT       = "[CRASH] New Win32 crash report"
47 EMAIL_BODY          = \
48 """
49 Dear Bug Squasher,
50
51 This crash has been reported automatically and might be incomplete and/or broken.
52 Windows version: %(WIN32_VERSION)s
53
54 %(STACKTRACE)s
55
56 Truly yours,
57 a python script.
58 """
59
60 import os, sys, re, tempfile
61 import string, shlex, subprocess
62 import smtplib, datetime, shutil
63 import traceback
64 from email.mime.text import MIMEText
65
66
67 def processFile(filename):
68     print "Processing " + filename
69     global win32_version
70
71     f = open(filename, 'r')
72     # Read (and repair) the input file
73     content = "".join(filter(lambda x: x in string.printable, f.read()))
74     f.close()
75
76     if os.path.getsize(filename) < 10:
77         print("File empty")
78         os.remove(filename)
79         return
80
81     # Check if VLC version match
82     if not isValidVersion(content):
83         print("Invalid VLC version")
84         moveFile(filename, outdated = True)
85         return
86
87     # Get Windows version
88     win32_version = getWinVersion(content) or 'unknown'
89
90     # Map eip <--> library
91     mapping = mapLibraries(content)
92     if not mapping:
93         print("Stacktrace not found")
94         os.remove(filename)
95         return
96
97     # Associate all eip to their respective lib
98     # lib1
99     #     `- 0x6904f020
100     #      - 0x6927d37c
101     # lib2
102     #     `- 0x7e418734
103     #      - 0x7e418816
104     #      - 0x7e42bf15
105     sortedEIP,delta_libs = sortEIP(content,mapping)
106     # Compute the stacktrace using GDB
107     eipmap = findSymbols(sortedEIP)
108     # Generate the body of the email
109     body = genEmailBody(mapping, eipmap, delta_libs)
110     # Send the email
111     sendEmail(body)
112     # Print the output
113     print(body)
114
115     # Finally archive the stacktrace
116     moveFile(filename, outdated = False)
117
118 def isValidVersion(content):
119     pattern = re.compile(r"^VLC=%s " % VLC_VERSION, re.MULTILINE)
120     res = pattern.search(content)
121     return True if res else False
122
123 def getWinVersion(content):
124     pattern = re.compile(r"^OS=(.*)$", re.MULTILINE)
125     res = pattern.search(content)
126     if res is not None:
127         return res.group(1)
128     return None
129
130 def getDiffAddress(content, name):
131     plugin_name_section = content.find(name)
132     if plugin_name_section < 0:
133         return None
134
135     begin_index = content.rfind("\n", 0, plugin_name_section) + 1
136     end_index = content.find("|", begin_index)
137
138     tmp_index = name.rfind('plugins\\')
139     libname = name[tmp_index :].replace("\\", "/")
140     full_path = VLC_BASE_DIR + libname
141
142     if not os.path.isfile(full_path):
143         return None
144
145     cmd = "objdump -p " + full_path + " |grep ImageBase -|cut -f2-"
146     p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).stdout.read().strip()
147
148     diff = int(content[begin_index:end_index], 16) - int(p, 16)
149     return diff
150
151 def mapLibraries(content):
152     stacktrace_section = content.find("[stacktrace]")
153     if stacktrace_section < 0:
154         return None
155
156     stacklines = content[stacktrace_section:]
157     stacklines = stacklines.splitlines()
158     pattern = re.compile(r"^([0-9a-fA-F]+)\|(.+)$")
159
160     mapping = []
161     for line in stacklines:
162         m = pattern.match(line)
163         print(line)
164         if m is not None:
165             mapping.append(m.group(1, 2))
166
167     if len(mapping) == 0:
168         return None
169     return mapping
170
171
172 def sortEIP(content, mapping):
173     # Merge all EIP mapping to the same library
174     libs = {}
175     libs_address = {}
176     for item in mapping:
177         # Extract the library name (without the full path)
178         index = item[1].rfind('\\')
179         libname = item[1][index + 1:]
180
181         # Append the eip to its respective lib
182         if libname not in libs:
183             libs[libname] = []
184             diff = getDiffAddress(content, item[1])
185             if diff is not None:
186                 libs_address[libname] = diff
187             else:
188                 libs_address[libname] = 0
189
190         libs[libname].append(int(item[0],16) - libs_address[libname])
191
192     return libs,libs_address
193
194
195 def findSymbols(sortedEIP):
196     eipmap = {}
197
198     for k, v in sortedEIP.items():
199         # Create the gdb batchfile
200         batchfile = tempfile.NamedTemporaryFile(mode="w")
201         batchfile.write("set print symbol-filename on\n")
202
203         # Append all eip for this lib
204         for eip in v:
205             batchfile.write('p/a %s\n' % hex(eip))
206         batchfile.flush()
207
208         # Generate the command line
209         cmd = GDB_CMD % {"VLC_BIN": VLC_BIN, "VLC_SYMBOLS_DIR": VLC_SYMBOLS_DIR, "DBG_FILE": k, "BATCH_FILE": batchfile.name}
210         args = shlex.split(cmd)
211
212         # Start GDB and get result
213         p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
214
215         # Parse result
216         gdb_pattern = re.compile(r"^\$\d+ = (.+)$")
217         cnt = 0
218         while p.poll() == None:
219             o = p.stdout.readline()
220             if o != b'':
221                 o = bytes.decode(o)
222                 m = gdb_pattern.match(o)
223                 if m is not None:
224                     #print("LINE: [%s]" % m.group(1))
225                     eipmap[v[cnt]] = m.group(1)
226                     cnt += 1
227         batchfile.close()
228     return eipmap
229
230
231 def genEmailBody(mapping, eipmap, delta_libs):
232     stacktrace = ""
233     cnt = 0
234     for item in mapping:
235         index = item[1].rfind('\\')
236         libname = item[1][index + 1:]
237         print(int(item[0],16), delta_libs[libname])
238         #print(eipmap)
239         #print(mapping)
240         stacktrace += "%d. %s [in %s]\n" % (cnt, eipmap[int(item[0],16)-delta_libs[libname]], item[1])
241         cnt += 1
242     stacktrace = stacktrace.rstrip('\n')
243     return EMAIL_BODY % {"STACKTRACE": stacktrace, "WIN32_VERSION": win32_version}
244
245
246 def sendEmail(body):
247     msg = MIMEText(body)
248     msg['Subject'] = EMAIL_SUBJECT
249     msg['From'] = EMAIL_FROM
250     msg['To'] = EMAIL_TO
251
252     # Send the email
253     s = smtplib.SMTP()
254     s.connect("127.0.0.1")
255     s.sendmail(EMAIL_FROM, [EMAIL_TO], msg.as_string())
256     s.quit()
257
258 def moveFile(filename, outdated = False):
259     today = datetime.datetime.now().strftime("%Y%m%d")
260     today_path = "%s/%s" % (WORKDIR, today)
261     if not os.path.isdir(today_path):
262         os.mkdir(today_path)
263     if not outdated:
264         shutil.move(filename, "%s/%s" % (today_path, os.path.basename(filename)))
265     else:
266         outdated_path = "%s/outdated/" % today_path
267         if not os.path.isdir(outdated_path):
268             os.mkdir(outdated_path)
269         shutil.move(filename, "%s/%s" % (outdated_path, os.path.basename(filename)))
270
271
272 ### ENTRY POINT ###
273
274 if len(sys.argv) == 1:
275     print("Folder mode")
276     batch = True
277 if len(sys.argv) != 2:
278     print("Running in batch mode")
279     batch = True
280 else:
281     batch = False
282
283 input_files = []
284 if not batch:
285     if not os.path.isfile(sys.argv[1]):
286         exit("file does not exists")
287     input_files.append(sys.argv[1])
288 else:
289     file_pattern = re.compile(FILE_MATCH)
290     entries = os.listdir(WORKDIR)
291     for entry in entries:
292         path_entry = WORKDIR + "/" + entry
293         if not os.path.isfile(path_entry):
294             continue
295         if not file_pattern.match(entry):
296             print(entry)
297             os.remove(path_entry)
298             continue
299         if os.path.getsize(path_entry) > FILE_MAX_SIZE:
300             print("%s is too big" % entry)
301             os.remove(path_entry)
302             continue
303         input_files.append(path_entry)
304
305 if not len(input_files):
306     exit("Nothing to process")
307
308 # Start processing each file
309 for input_file in input_files:
310     try:
311         processFile(input_file)
312     except Exception as ex:
313         print(traceback.format_exc())
314