Chromium Code Reviews| Index: tools/grokdump.py |
| diff --git a/tools/grokdump.py b/tools/grokdump.py |
| index a5a2ae08a879b11447352a262e02222da4e7933b..43a5c7cb413ddffd6f24aabe1e9f0000aca0fb9a 100755 |
| --- a/tools/grokdump.py |
| +++ b/tools/grokdump.py |
| @@ -28,6 +28,7 @@ |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| import bisect |
| +import cgi |
| import cmd |
| import codecs |
| import ctypes |
| @@ -39,8 +40,16 @@ import os |
| import re |
| import struct |
| import sys |
| +import time |
| import types |
| +import urllib |
| import v8heapconst |
| +import webbrowser |
| +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer |
| +from urlparse import urlparse, parse_qs |
| + |
| +PORT_NUMBER = 8081 |
| + |
| USAGE="""usage: %prog [OPTIONS] [DUMP-FILE] |
| @@ -701,6 +710,20 @@ class MinidumpReader(object): |
| reader.FormatIntPtr(word)) |
| self.ForEachMemoryRegion(search_inside_region) |
| + def FindWordList(self, word): |
| + aligned_res = [ ] |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: the style guide doesn't like spaces inside em
Jarin
2014/03/26 10:54:28
Done.
|
| + unaligned_res = [ ] |
| + def search_inside_region(reader, start, size, location): |
| + for loc in xrange(location, location + size - self.PointerSize()): |
| + if reader._ReadWord(loc) == word: |
| + slot = start + (loc - location) |
| + if slot % self.PointerSize() == 0: |
| + aligned_res.append(slot) |
| + else: |
| + unaligned_res.append(slot) |
| + self.ForEachMemoryRegion(search_inside_region) |
| + return (aligned_res, unaligned_res) |
| + |
| def FindLocation(self, address): |
| offset = 0 |
| if self.memory_list64 is not None: |
| @@ -930,8 +953,9 @@ class HeapObject(object): |
| def SmiField(self, offset): |
| field_value = self.heap.reader.ReadUIntPtr(self.address + offset) |
| - assert (field_value & 1) == 0 |
| - return field_value / 2 |
| + if (field_value & 1) == 0: |
| + return field_value / 2 |
| + return None |
| class Map(HeapObject): |
| @@ -1348,7 +1372,9 @@ class JSFunction(HeapObject): |
| if not self.shared.script.Is(Script): return source |
| script_source = self.shared.script.source |
| if not script_source.Is(String): return source |
| - return script_source.GetChars()[start:end] |
| + if start and end: |
| + source = script_source.GetChars()[start:end] |
| + return source |
| class SharedFunctionInfo(HeapObject): |
| @@ -1382,7 +1408,10 @@ class SharedFunctionInfo(HeapObject): |
| else: |
| start_position_and_type = \ |
| self.SmiField(self.StartPositionAndTypeOffset()) |
| - self.start_position = start_position_and_type >> 2 |
| + if start_position_and_type: |
| + self.start_position = start_position_and_type >> 2 |
| + else: |
| + self.start_position = None |
| self.end_position = \ |
| self.SmiField(self.EndPositionOffset()) |
| @@ -1560,6 +1589,81 @@ class KnownMap(HeapObject): |
| def __str__(self): |
| return "<%s>" % self.known_name |
| +COMMENT_RE = re.compile(r"^C (0x[0-9a-fA-F]+) (.*)$") |
|
Jakob Kummerow
2014/03/24 15:21:32
style nit: two empty lines between top-level thing
Jarin
2014/03/26 10:54:28
Done.
|
| +PAGEADDRESS_RE = re.compile( |
| + r"^P (mappage|pointerpage|datapage) (0x[0-9a-fA-F]+)$") |
| + |
| +class InspectionInfo(object): |
| + def __init__(self, minidump_name, reader): |
| + self.cmt_file = minidump_name + ".comments" |
| + self.address_comments = { } |
| + self.page_address = { } |
| + print "Opening comment file '%s'" % self.cmt_file |
| + try: |
| + f = open(self.cmt_file, "r") |
|
Jakob Kummerow
2014/03/24 15:21:32
The pythonic way to express open/close sequences i
Jarin
2014/03/26 10:54:28
Done.
|
| + lines = f.readlines() |
| + f.close() |
| + |
| + for l in lines: |
| + m = COMMENT_RE.match(l) |
| + if m: |
| + self.address_comments[int(m.group(1), 0)] = m.group(2) |
| + m = PAGEADDRESS_RE.match(l) |
| + if m: |
| + self.page_address[m.group(1)] = int(m.group(2), 0) |
| + except IOError: |
| + print "No comments file, starting a new one" |
| + self.reader = reader |
| + self.color_addresses() |
| + return |
| + |
| + def get_page_address(self, page_kind): |
| + return self.page_address.get(page_kind, 0) |
| + |
| + def save_page_address(self, page_kind, address): |
| + f = open(self.cmt_file, "a") |
|
Jakob Kummerow
2014/03/24 15:21:32
again, prefer "with"
Jarin
2014/03/26 10:54:28
Done.
|
| + f.write("P %s 0x%x\n" % (page_kind, address)) |
| + f.close() |
| + |
| + def color_addresses(self): |
| + self.styles = { } |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: I have a weak preference to initialize all me
Jarin
2014/03/26 10:54:28
Done.
|
| + |
| + # Color all stack addresses |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: trailing full stop please
Jarin
2014/03/26 10:54:28
Done.
|
| + exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
| + stack_top = self.reader.ExceptionSP() |
| + stack_bottom = exception_thread.stack.start + \ |
| + exception_thread.stack.memory.data_size |
| + frame_pointer = self.reader.ExceptionFP() |
| + self.styles[frame_pointer] = "frame" |
| + for slot in xrange(stack_top, stack_bottom, self.reader.PointerSize()): |
| + self.styles[slot] = "stackaddress" |
| + for slot in xrange(stack_top, stack_bottom, self.reader.PointerSize()): |
| + maybe_address = self.reader.ReadUIntPtr(slot) |
| + self.styles[maybe_address] = "stackval" |
| + if slot == frame_pointer: |
| + self.styles[slot] = "frame" |
| + frame_pointer = maybe_address |
| + self.styles[self.reader.ExceptionIP()] = "pc" |
| + |
| + def get_style_class(self, address): |
| + return self.styles.get(address, None) |
| + |
| + def get_style_class_string(self, address): |
| + style = self.get_style_class(address) |
| + if style != None: |
| + return " class=\"%s\" " % style |
| + else: |
| + return "" |
| + |
| + def set_comment(self, address, comment): |
| + self.address_comments[address] = comment |
| + f = open(self.cmt_file, "a") |
|
Jakob Kummerow
2014/03/24 15:21:32
with
Jarin
2014/03/26 10:54:28
Done.
|
| + f.write("C 0x%x %s\n" % (address, comment)) |
| + f.close() |
| + |
| + def get_comment(self, address): |
| + return self.address_comments.get(address, "") |
| + |
| class InspectionPadawan(object): |
| """The padawan can improve annotations by sensing well-known objects.""" |
| @@ -1653,6 +1757,1059 @@ class InspectionPadawan(object): |
| self.reader.FormatIntPtr(self.known_first_data_page), |
| self.reader.FormatIntPtr(self.known_first_pointer_page)) |
| +WEB_HEADER = """ |
| +<!DOCTYPE html> |
| +<html> |
| +<head> |
| +<meta content="text/html; charset=ISO-8859-1" http-equiv="content-type"> |
| +<style media="screen" type="text/css"> |
| + |
| +.code {{ |
|
Jakob Kummerow
2014/03/24 15:21:32
If you used %s-placeholders ("foo %s baz" % bar),
Jarin
2014/03/26 10:54:28
However, with %s you cannot reuse the same argumen
|
| + font-family: monospace; |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: funky indentation
Jarin
2014/03/26 10:54:28
Done.
|
| +}} |
| + |
| +.dmptable {{ |
| + border-collapse : collapse; |
| + border-spacing : 0px; |
| +}} |
| + |
| +.codedump {{ |
| + border-collapse : collapse; |
| + border-spacing : 0px; |
| +}} |
| + |
| +.addrcomments {{ |
| + border : 0px; |
| +}} |
| + |
| +.register {{ |
| + padding-right : 1em; |
| +}} |
| + |
| +.header {{ |
| + clear : both; |
| +}} |
| + |
| +.header .navigation {{ |
| + float : left; |
| +}} |
| + |
| +.header .dumpname {{ |
| + float : right; |
| +}} |
| + |
| +tr.highlight-line {{ |
| + background-color : yellow; |
| +}} |
| + |
| +.highlight {{ |
| + background-color : magenta; |
| +}} |
| + |
| +tr.inexact-highlight-line {{ |
| + background-color : pink; |
| +}} |
| + |
| +input {{ |
| + background-color: inherit; |
| + border: 1px solid LightGray; |
| +}} |
| + |
| +.dumpcomments {{ |
| + border : 1px solid LightGray; |
| + width : 32em; |
| +}} |
| + |
| +.regions td {{ |
| + padding:0 15px 0 15px; |
| +}} |
| + |
| +.stackframe td {{ |
| + background-color : cyan; |
| +}} |
| + |
| +.pc {{ |
| +}} |
|
Jakob Kummerow
2014/03/24 15:21:32
Is this intentionally empty?
Jarin
2014/03/26 10:54:28
Removed.
|
| + |
| +.stackaddress {{ |
| + background-color : LightGray; |
| +}} |
| + |
| +.stackval {{ |
| + background-color : LightCyan; |
| +}} |
| + |
| +.frame {{ |
| + background-color : cyan; |
| +}} |
| + |
| +.commentinput {{ |
| + width : 20em; |
| +}} |
| + |
| +a.nodump:visited {{ |
| + color : black; |
| + text-decoration : none; |
| +}} |
| + |
| +a.nodump:link {{ |
| + color : black; |
| + text-decoration : none; |
| +}} |
| + |
| +a:visited {{ |
| + color : blueviolet; |
| +}} |
| + |
| +a:link {{ |
| + color : blue; |
| +}} |
| + |
| +.disasmcmt {{ |
| + color : DarkGreen; |
| +}} |
| + |
| +</style> |
| + |
| +<script type="application/javascript"> |
| + |
| +var address_str = "address-"; |
| +var address_len = address_str.length; |
| + |
| +function cmt() {{ |
| + var s = event.srcElement.id; |
| + var index = s.indexOf(address_str); |
| + if (index >= 0) {{ |
| + send_comment(s.substring(index + address_len), event.srcElement.value); |
| + }} |
| +}} |
| + |
| +function send_comment(address, comment) {{ |
| + xmlhttp = new XMLHttpRequest(); |
| + address = encodeURIComponent(address) |
| + comment = encodeURIComponent(comment) |
| + xmlhttp.open("GET", |
|
Jakob Kummerow
2014/03/24 15:21:32
I think using GET to perform actions is considered
Jarin
2014/03/26 10:54:28
Yeah, I am leaving as is.
|
| + "setcomment?{0}&address=" + address + |
| + "&comment=" + comment, true); |
| + xmlhttp.send(); |
| +}} |
| + |
| +var dump_str = "dump-"; |
| +var dump_len = dump_str.length; |
| + |
| +function dump_cmt() {{ |
| + var s = event.srcElement.id; |
| + var index = s.indexOf(dump_str); |
| + if (index >= 0) {{ |
| + send_dump_desc(s.substring(index + dump_len), event.srcElement.value); |
| + }} |
| +}} |
| + |
| +function send_dump_desc(name, desc) {{ |
| + xmlhttp = new XMLHttpRequest(); |
| + name = encodeURIComponent(name) |
| + desc = encodeURIComponent(desc) |
| + xmlhttp.open("GET", |
| + "setdumpdesc?dump=" + name + |
| + "&description=" + desc, true); |
| + xmlhttp.send(); |
| +}} |
| + |
| +function onpage(kind, address) {{ |
| + xmlhttp = new XMLHttpRequest(); |
| + kind = encodeURIComponent(kind) |
| + address = encodeURIComponent(address) |
| + xmlhttp.onreadystatechange = function() {{ |
| + if (xmlhttp.readyState==4 && xmlhttp.status==200) |
| + location.reload(true) |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: {} around the block please
Jarin
2014/03/26 10:54:28
Done.
|
| + }}; |
| + xmlhttp.open("GET", |
| + "setpageaddress?{0}&kind=" + kind + |
| + "&address=" + address); |
| + xmlhttp.send(); |
| +}} |
| + |
| +function main() {{ |
|
Jakob Kummerow
2014/03/24 15:21:32
looks like we don't need this urgently.
Jarin
2014/03/26 10:54:28
Removed.
|
| +}} |
| + |
| +</script> |
| + |
| +<title>Dump {1}</title> |
| +</head> |
| + |
| +<body onload="main()"> |
| + <div class="header"> |
| + <form class="navigation" action="search.html"> |
| + <a href="summary.html?{0}">Context info</a>   |
| + <a href="info.html?{0}">Dump info</a>   |
| + <a href="modules.html?{0}">Modules</a> |
| + |
| + <input type="search" name="val"> |
| + <input type="submit" name="search" value="Search"> |
| + <input type="hidden" name="dump" value="{1}"> |
| + </form> |
| + <form class="navigation" action="disasm.html#highlight"> |
| + |
| + |
| + |
| + <input type="search" name="val"> |
| + <input type="submit" name="disasm" value="Disasm"> |
| + |
| + |
| + |
| + <a href="dumps.html">Dumps...</a> |
| + </form> |
| + <!-- |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: remove this if it's not needed
Jarin
2014/03/26 10:54:28
Done.
|
| + <form class="dumpname" action="dump.html"> |
| + Dump id: |
| + <input type="search" name="dumpid"> |
| + <input type="submit" name="action" value="Go"> |
| + <input type="hidden" name="dump" value="{1}"> |
| + </form> |
| + --> |
| + </div> |
| + <br> |
| + <hr> |
| +""" |
| + |
| +WEB_FOOTER = """ |
| +</body> |
| +</html> |
| +""" |
| + |
| +class WebParameterError(Exception): |
|
Jakob Kummerow
2014/03/24 15:21:32
again, two empty lines before and after top-level
Jarin
2014/03/26 10:54:28
Done.
|
| + def __init__(self, message): |
| + Exception.__init__(self, message) |
| + |
| +class InspectionWebHandler(BaseHTTPRequestHandler): |
| + def fmt(self, qc): |
|
Jakob Kummerow
2014/03/24 15:21:32
naming nit: abbreviations are generally frowned up
Jarin
2014/03/26 10:54:28
Done.
|
| + name = qc.get("dump", [None])[0] |
| + return self.server.get_dump_formatter(name) |
| + |
| + def send_success_html_headers(self): |
| + self.send_response(200) |
| + self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") |
| + self.send_header("Pragma", "no-cache") |
| + self.send_header("Expires", "0") |
| + self.send_header('Content-type','text/html') |
| + self.end_headers() |
| + return |
| + |
| + def do_GET(self): |
| + try: |
| + print "GET!" |
|
Jakob Kummerow
2014/03/24 15:21:32
Looks like a debugging aid. Is this still needed?
Jarin
2014/03/26 10:54:28
Removed.
|
| + parsedurl = urlparse(self.path) |
| + qc = parse_qs(parsedurl.query) |
| + if parsedurl.path == "/dumps.html": |
| + self.send_success_html_headers() |
| + self.server.output_dumps(self.wfile) |
| + return |
| + if parsedurl.path == "/summary.html": |
|
Jakob Kummerow
2014/03/24 15:21:32
s/if/elif/ like below? In turn you could omit all
|
| + self.send_success_html_headers() |
| + self.fmt(qc).output_summary(self.wfile) |
| + return |
| + if parsedurl.path == "/info.html": |
| + self.send_success_html_headers() |
| + self.fmt(qc).output_info(self.wfile) |
| + return |
| + elif parsedurl.path == "/modules.html": |
| + self.send_success_html_headers() |
| + self.server.formatter.output_modules(self.wfile) |
| + return |
| + elif parsedurl.path == "/search.html": |
| + address = qc.get("val", []) |
| + if len(address) != 1: |
| + self.send_error(404, "Invalid params") |
| + return |
| + if qc.get("search", None) != None: |
| + self.send_success_html_headers() |
| + self.fmt(qc).output_search_res(self.wfile, address[0]) |
| + return |
| + elif parsedurl.path == "/disasm.html": |
| + address = qc.get("val", []) |
| + exact = qc.get("exact", ["on"]) |
| + if len(address) != 1: |
| + self.send_error(404, "Invalid params") |
| + return |
| + self.send_success_html_headers() |
| + self.fmt(qc).output_disasm(self.wfile, address[0], exact[0]) |
| + return |
| + elif parsedurl.path == "/data.html": |
| + address = qc.get("val", []) |
| + datakind = qc.get("type", ["address"]) |
| + if len(address) == 1 and len(datakind) == 1: |
| + self.send_success_html_headers() |
| + self.fmt(qc).output_data(self.wfile, address[0], datakind[0]) |
| + else: |
| + self.send_error(404,'Invalid params') |
| + return |
| + elif parsedurl.path == "/setdumpdesc": |
| + name = qc.get("dump", [""]) |
| + description = qc.get("description", [""]) |
| + |
| + if len(name) == 1 and len(description) == 1: |
| + name = name[0] |
| + description = description[0] |
| + if self.server.set_dump_desc(name, description): |
| + self.send_success_html_headers() |
| + self.wfile.write("OK") |
| + return |
| + self.send_error(404,'Invalid params') |
|
Jakob Kummerow
2014/03/24 15:21:32
again, I'd prefer if+else over if+return
Jarin
2014/03/26 10:54:28
Done.
|
| + return |
| + |
| + elif parsedurl.path == "/setcomment": |
| + address = qc.get("address", []) |
| + comment = qc.get("comment", [""]) |
| + |
| + if len(address) == 1 and len(comment) == 1: |
| + address = address[0] |
| + comment = comment[0] |
| + self.fmt(qc).set_comment(address, comment) |
| + self.send_success_html_headers() |
| + self.wfile.write("OK") |
| + else: |
| + self.send_error(404,'Invalid params') |
| + return |
| + |
| + elif parsedurl.path == "/setpageaddress": |
| + kind = qc.get("kind", []) |
| + address = qc.get("address", [""]) |
| + |
| + if len(kind) == 1 and len(address) == 1: |
| + kind = kind[0] |
| + address = address[0] |
| + self.fmt(qc).set_page_address(kind, address) |
| + self.send_success_html_headers() |
| + self.wfile.write("OK") |
| + else: |
| + self.send_error(404,'Invalid params') |
| + return |
| + |
| + else: |
| + self.send_error(404,'File Not Found: %s' % self.path) |
| + |
| + except IOError: |
| + self.send_error(404,'File Not Found: %s' % self.path) |
| + |
| + except WebParameterError as e: |
| + self.send_error(404, 'Web parameter error: %s' % e.message) |
| + |
| +HTML_REG_FORMAT = "<span class=\"register\"><b>%s</b>: %s</span>\n" |
| + |
| +class InspectionWebFormatter(object): |
| + CONTEXT_FULL = 0 |
| + CONTEXT_SHORT = 1 |
| + |
| + def __init__(self, options, minidump_name, server): |
| + self.dumpfilename = os.path.split(minidump_name)[1] |
| + self.encfilename = urllib.urlencode({ 'dump' : self.dumpfilename }) |
| + self.reader = MinidumpReader(options, minidump_name) |
| + self.server = server |
| + |
| + # Set up the heap |
| + exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
| + stack_top = self.reader.ExceptionSP() |
| + stack_bottom = exception_thread.stack.start + \ |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: the style guide prefers () over trailing \, i
|
| + exception_thread.stack.memory.data_size |
| + stack_map = {self.reader.ExceptionIP(): -1} |
| + for slot in xrange(stack_top, stack_bottom, self.reader.PointerSize()): |
| + maybe_address = self.reader.ReadUIntPtr(slot) |
| + if not maybe_address in stack_map: |
| + stack_map[maybe_address] = slot |
| + self.heap = V8Heap(self.reader, stack_map) |
| + |
| + self.padawan = InspectionPadawan(self.reader, self.heap) |
| + self.comments = InspectionInfo(minidump_name, self.reader) |
| + self.padawan.known_first_data_page = \ |
| + self.comments.get_page_address("datapage") |
| + self.padawan.known_first_map_page = \ |
| + self.comments.get_page_address("mappage") |
| + self.padawan.known_first_pointer_page = \ |
| + self.comments.get_page_address("pointerpage") |
| + |
| + def set_comment(self, straddress, comment): |
| + try: |
| + print "Adding comment to dump '%s': %s -> '%s'" % ( |
|
Jakob Kummerow
2014/03/24 15:21:32
Still needed? If you don't want to delete it, cons
Jarin
2014/03/26 10:54:28
Removed.
|
| + self.dumpfilename, straddress, comment) |
| + address = int(straddress, 0) |
| + self.comments.set_comment(address, comment) |
| + except ValueError: |
| + print "Invalid address" |
| + |
| + def set_page_address(self, kind, straddress): |
| + try: |
| + print "Adding page annotation to dump '%s': %s -> %s" % ( |
|
Jakob Kummerow
2014/03/24 15:21:32
still needed?
Jarin
2014/03/26 10:54:28
Removed.
|
| + self.dumpfilename, kind, straddress) |
| + address = int(straddress, 0) |
| + if kind == "datapage": |
| + self.padawan.known_first_data_page = address |
| + elif kind == "mappage": |
| + self.padawan.known_first_map_page = address |
| + elif kind == "pointerpage": |
| + self.padawan.known_first_pointer_page = address |
| + self.comments.save_page_address(kind, address) |
| + except ValueError: |
| + print "Invalid address" |
| + |
| + def td_from_address(self, f, address): |
| + f.write("<td %s>" % self.comments.get_style_class_string(address)) |
| + |
| + def format_address(self, maybeaddress, straddress = None): |
| + if maybeaddress == None: |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: the style guide wants "is None" instead of "=
|
| + return "not in dump" |
| + else: |
| + if straddress == None: |
| + straddress = "0x" + self.reader.FormatIntPtr(maybeaddress) |
| + style_class = "" |
| + if not self.reader.IsValidAddress(maybeaddress): |
| + style_class = " class=\"nodump\"" |
| + return "<a %s href=\"search.html?%s&val=%s&search=yes\">%s</a>" % ( |
| + style_class, self.encfilename, straddress, straddress) |
| + |
| + def output_header(self, f): |
| + f.write(WEB_HEADER.format(self.encfilename, cgi.escape(self.dumpfilename))) |
| + |
| + def output_footer(self, f): |
| + f.write(WEB_FOOTER) |
| + |
| + MAX_CONTEXT_STACK = 4096 |
| + |
| + def output_summary(self, f): |
| + self.output_header(f) |
| + f.write('<div class="code">') |
| + self.output_context(f, InspectionWebFormatter.CONTEXT_SHORT) |
| + self.output_disasm_pc(f) |
| + |
| + # Output stack |
| + exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
| + stack_bottom = exception_thread.stack.start + \ |
| + min(exception_thread.stack.memory.data_size, self.MAX_CONTEXT_STACK) |
| + stack_top = self.reader.ExceptionSP() |
| + self.output_words(f, stack_top - 16, stack_bottom, stack_top, "Stack") |
| + |
| + f.write('</div>') |
| + self.output_footer(f) |
| + return |
| + |
| + def output_info(self, f): |
| + self.output_header(f) |
| + f.write("<h3>Dump info</h3>\n") |
| + f.write("Description: ") |
| + self.server.output_dump_desc_field(f, self.dumpfilename) |
| + f.write("<br>\n") |
| + f.write("Filename: ") |
| + f.write("<span class=\"code\">%s</span><br>\n" % (self.dumpfilename)) |
| + dt = datetime.datetime.fromtimestamp(self.reader.header.time_date_stampt) |
| + f.write("Timestamp: %s<br>\n" % dt.strftime('%Y-%m-%d %H:%M:%S')) |
| + self.output_context(f, InspectionWebFormatter.CONTEXT_FULL) |
| + self.output_address_ranges(f) |
| + self.output_footer(f) |
| + return |
| + |
| + def output_address_ranges(self, f): |
| + regions = { } |
| + def print_region(reader, start, size, location): |
| + regions[start] = size |
| + self.reader.ForEachMemoryRegion(print_region) |
| + f.write("<h3>Available memory regions</h3>\n") |
| + f.write('<div class="code">') |
| + f.write("<table class=\"regions\">\n") |
| + f.write("<thead><tr>") |
| + f.write("<th>Start address</th>") |
| + f.write("<th>End address</th>") |
| + f.write("<th>Number of bytes</th>") |
| + f.write("</tr></thead>") |
| + f.write("</thead>\n") |
| + for start in sorted(regions): |
| + size = regions[start] |
| + f.write("<tr>") |
| + f.write("<td>%s</td>" % self.format_address(start)) |
| + f.write("<td> %s</td>" % self.format_address(start + size)) |
| + f.write("<td> %d</td>" % size) |
| + f.write("</tr>\n") |
| + f.write("</table>\n") |
| + f.write('</div>') |
| + return |
| + |
| + def output_module_details(self, f, module): |
| + f.write("<b>%s</b>" % GetModuleName(self.reader, module)) |
| + file_version = GetVersionString(module.version_info.dwFileVersionMS, |
| + module.version_info.dwFileVersionLS) |
| + product_version = GetVersionString(module.version_info.dwProductVersionMS, |
| + module.version_info.dwProductVersionLS) |
| + f.write("<br> \n") |
| + f.write("base: %s" % self.reader.FormatIntPtr(module.base_of_image)) |
| + f.write("<br> \n") |
| + f.write(" end: %s" % self.reader.FormatIntPtr(module.base_of_image + |
| + module.size_of_image)) |
| + f.write("<br> \n") |
| + f.write(" file version: %s" % file_version) |
| + f.write("<br> \n") |
| + f.write(" product version: %s" % product_version) |
| + f.write("<br> \n") |
| + time_date_stamp = datetime.datetime.fromtimestamp(module.time_date_stamp) |
| + f.write(" timestamp: %s" % time_date_stamp) |
| + f.write("<br>\n"); |
| + |
| + def output_modules(self, f): |
| + self.output_header(f) |
| + f.write('<div class="code">') |
| + for module in self.reader.module_list.modules: |
| + self.output_module_details(f, module) |
| + f.write("</div>") |
| + self.output_footer(f) |
| + return |
| + |
| + def output_context(self, f, details): |
| + exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
| + f.write("<h3>Exception context</h3>") |
| + f.write('<div class="code">\n') |
| + f.write("Thread id: %d" % exception_thread.id) |
| + f.write(" Exception code: %08X\n" % ( |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: I'd break as:
f.write(" Excep
Jarin
2014/03/26 10:54:28
Done.
|
| + self.reader.exception.exception.code)) |
| + if details == InspectionWebFormatter.CONTEXT_FULL: |
| + if self.reader.exception.exception.parameter_count > 0: |
| + f.write(" Exception parameters: \n") |
| + for i in xrange(0, self.reader.exception.exception.parameter_count): |
| + f.write("%08x" % self.reader.exception.exception.information[i]) |
| + f.write("<br><br>\n") |
| + |
| + for r in CONTEXT_FOR_ARCH[self.reader.arch]: |
| + f.write(HTML_REG_FORMAT % ( |
| + r, self.format_address(self.reader.Register(r)))) |
| + # TODO(vitalyr): decode eflags. |
| + if self.reader.arch == MD_CPU_ARCHITECTURE_ARM: |
| + f.write("<b>cpsr</b>: %s" % bin(self.reader.exception_context.cpsr)[2:]) |
| + else: |
| + f.write("<b>eflags</b>: %s" % ( |
| + bin(self.reader.exception_context.eflags)[2:])) |
| + f.write('</div>\n') |
| + return |
| + |
| + def align_down(self, a, size): |
| + alignment_correction = a % size |
| + return a - alignment_correction |
| + |
| + def align_up(self, a, size): |
| + alignment_correction = (size - 1) - ( |
| + (a + size - 1) % size) |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: fits on previous line?
Jarin
2014/03/26 10:54:28
Done.
|
| + return a + alignment_correction |
| + |
| + |
| + def format_object(self, address): |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: one empty line is enough between non-toplevel
Jarin
2014/03/26 10:54:28
Done.
|
| + heap_object = self.padawan.SenseObject(address) |
| + return cgi.escape(str(heap_object or "")) |
| + |
| + |
| + def output_data(self, f, straddress, datakind): |
| + try: |
| + self.output_header(f) |
| + address = int(straddress, 0) |
| + if not self.reader.IsValidAddress(address): |
| + f.write("<h3>Address 0x%x not found in the dump.</h3>" % address) |
| + return |
| + region = self.reader.FindRegion(address) |
| + if datakind == "address": |
| + self.output_words(f, region[0], region[0] + region[1], address, "Dump") |
| + elif datakind == "ascii": |
| + self.output_ascii(f, region[0], region[0] + region[1], address) |
| + self.output_footer(f) |
| + |
| + except ValueError: |
| + f.write("<h3>Unrecognized address format \"%s\".</h3>" % straddress) |
| + return |
| + |
| + |
| + def output_words(self, f, start_address, end_address, \ |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: trailing \ is not necessary
Jarin
2014/03/26 10:54:28
Done.
|
| + highlight_address, desc): |
| + region = self.reader.FindRegion(highlight_address) |
| + if region == None: |
| + f.write("<h3>Address 0x%x not found in the dump.</h3>\n" % ( |
| + highlight_address)) |
| + return |
| + size = self.heap.PointerSize() |
| + start_address = self.align_down(start_address, size) |
| + low = self.align_down(region[0], size) |
| + high = self.align_up(region[0] + region[1], size) |
| + if start_address < low: |
| + start_address = low |
| + end_address = self.align_up(end_address, size) |
| + if end_address > high: |
| + end_address = high |
| + |
| + expand = "" |
| + if start_address != low or end_address != high: |
| + expand = "(<a href=\"data.html?%s&val=0x%x#highlight\">more...</a>)" % ( |
| + self.encfilename, highlight_address) |
| + |
| + f.write(("<h3>%s 0x%x - 0x%x, " |
| + "highlighting <a href=\"#highlight\">0x%x</a> %s</h3>\n") % ( |
| + desc, start_address, end_address, highlight_address, expand)) |
| + f.write('<div class="code">') |
| + f.write("<table class=\"codedump\"" |
| + "cellspacing=\"0\" cellpadding=\"0\">\n") |
| + |
| + for slot in xrange(start_address, |
| + end_address, |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: fits on one line?
Jarin
2014/03/26 10:54:28
Done.
|
| + size): |
| + heap_object = "" |
| + maybe_address = None |
| + end_region = region[0] + region[1] |
| + if slot < region[0] or slot + size > end_region: |
| + straddress = "0x" |
| + for i in xrange(end_region, slot + size): |
| + straddress += "??" |
| + for i in reversed( |
| + xrange(max(slot, region[0]), min(slot + size, end_region))): |
| + straddress += "%02x" % self.reader.ReadU8(i) |
| + for i in xrange(slot, region[0]): |
| + straddress += "??" |
| + else: |
| + maybe_address = self.reader.ReadUIntPtr(slot) |
| + straddress = self.format_address(maybe_address) |
| + if maybe_address: |
| + heap_object = self.format_object(maybe_address) |
| + |
| + address_fmt = "%s </td>\n" |
| + if slot == highlight_address: |
| + f.write("<tr class=\"highlight-line\">\n") |
| + address_fmt = "<a name=\"highlight\"></a>%s </td>\n" |
| + if slot <= highlight_address and highlight_address < slot + size: |
| + f.write("<tr class=\"inexact-highlight-line\">\n") |
| + address_fmt = "<a name=\"highlight\"></a>%s </td>\n" |
| + else: |
| + f.write("<tr>\n") |
| + |
| + f.write(" <td>") |
| + self.output_comment_box(f, "da-", slot) |
| + f.write(" </td>\n") |
| + self.td_from_address(f, slot) |
| + f.write(address_fmt % self.format_address(slot)) |
| + self.td_from_address(f, maybe_address) |
| + f.write(": %s </td>\n" % straddress) |
| + f.write(" <td>") |
| + if maybe_address != None: |
| + self.output_comment_box( |
| + f, "sv-" + self.reader.FormatIntPtr(slot), maybe_address) |
| + f.write(" </td>\n") |
| + f.write(" <td>%s</td>\n" % (heap_object or '')) |
| + f.write("</tr>\n") |
| + f.write("</table>\n") |
| + f.write("</div>") |
| + return |
| + |
| + def output_ascii(self, f, start_address, end_address, highlight_address): |
| + region = self.reader.FindRegion(highlight_address) |
| + if region == None: |
| + f.write("<h3>Address %x not found in the dump.</h3>" % |
| + highlight_address) |
| + return |
| + if start_address < region[0]: |
| + start_address = region[0] |
| + if end_address > region[0] + region[1]: |
| + end_address = region[0] + region[1] |
| + |
| + expand = "" |
| + if start_address != region[0] or end_address != region[0] + region[1]: |
| + expand = ("(<a href=\"data.html?%s&val=0x%x&type=ascii#highlight\">" |
| + "more...</a>)") % ( |
| + self.encfilename, highlight_address) |
| + |
| + f.write("<h3>ASCII dump 0x%x - 0x%x, highlighting 0x%x %s</h3>" % ( |
| + start_address, end_address, highlight_address, expand)) |
| + |
| + line_width = 64 |
| + |
| + f.write('<div class="code">') |
| + |
| + start = self.align_down(start_address, line_width) |
| + |
| + for address in xrange(start, end_address): |
| + if address % 64 == 0: |
| + if address != start: |
| + f.write("<br>") |
| + f.write("0x%08x: " % address) |
| + if address < start_address: |
| + f.write(" ") |
| + else: |
| + if address == highlight_address: |
| + f.write("<span class=\"highlight\">") |
| + code = self.reader.ReadU8(address) |
| + if code < 127 and code >= 32: |
| + f.write("&#") |
| + f.write(str(code)) |
| + else: |
| + f.write("·") |
|
Jakob Kummerow
2014/03/24 15:21:32
Doesn't this need s/·/·/ ?
Jarin
2014/03/26 10:54:28
Done (the char code above needs ';', too)
|
| + if address == highlight_address: |
| + f.write("</span>") |
| + f.write("</div>") |
| + return |
| + |
| + def output_disasm(self, f, straddress, strexact): |
| + try: |
| + self.output_header(f) |
| + address = int(straddress, 0) |
| + if not self.reader.IsValidAddress(address): |
| + f.write("<h3>Address 0x%x not found in the dump.</h3>" % address) |
| + return |
| + region = self.reader.FindRegion(address) |
| + self.output_disasm_range( |
| + f, region[0], region[0] + region[1], address, strexact == "on") |
| + self.output_footer(f) |
| + except ValueError: |
| + f.write("<h3>Unrecognized address format \"%s\".</h3>" % straddress) |
| + return |
| + |
| + def output_disasm_range( |
| + self, f, start_address, end_address, highlight_address, exact): |
| + region = self.reader.FindRegion(highlight_address) |
| + if start_address < region[0]: |
| + start_address = region[0] |
| + if end_address > region[0] + region[1]: |
| + end_address = region[0] + region[1] |
| + count = end_address - start_address |
| + lines = self.reader.GetDisasmLines(start_address, count) |
| + found = False |
| + if exact: |
| + for line in lines: |
| + if line[0] + start_address == highlight_address: |
| + found = True |
| + break |
| + if not found: |
| + start_address = highlight_address |
| + count = end_address - start_address |
| + lines = self.reader.GetDisasmLines(highlight_address, count) |
| + expand = "" |
| + if start_address != region[0] or end_address != region[0] + region[1]: |
| + exactness = "" |
| + if exact and not found and end_address == region[0] + region[1]: |
| + exactness = "&exact=off" |
| + expand = ("(<a href=\"disasm.html?%s%s" |
| + "&val=0x%x#highlight\">more...</a>)") % ( |
| + self.encfilename, exactness, highlight_address) |
| + |
| + f.write("<h3>Disassembling 0x%x - 0x%x, highlighting 0x%x %s</h3>" % ( |
| + start_address, end_address, highlight_address, expand)) |
| + f.write('<div class="code">') |
| + f.write("<table class=\"codedump\" cellspacing=\"0\" cellpadding=\"0\">\n"); |
| + for i in xrange(0, len(lines)): |
| + line = lines[i] |
| + next_address = count |
| + if i + 1 < len(lines): |
| + next_line = lines[i + 1] |
| + next_address = next_line[0] |
| + self.format_disasm_line( |
| + f, start_address, line, next_address, highlight_address) |
| + f.write("</table>\n") |
| + f.write("</div>") |
| + return |
| + |
| + def annotate_disasm_addresses(self, line): |
| + extra = [] |
| + for m in ADDRESS_RE.finditer(line): |
| + maybe_address = int(m.group(0), 16) |
| + formatted_address = self.format_address(maybe_address, m.group(0)) |
| + line = line.replace(m.group(0), formatted_address) |
| + object = self.padawan.SenseObject(maybe_address) |
| + if not object: continue |
| + extra.append(cgi.escape(str(object))) |
| + if len(extra) == 0: return line |
| + return "%s <span class=\"disasmcmt\">;; %s</span>" % ( |
| + line, ", ".join(extra)) |
| + |
| + |
| + def format_disasm_line( |
| + self, f, start, line, next_address, highlight_address): |
| + line_address = start + line[0] |
| + address_fmt = " <td>%s</td>\n" |
| + if line_address == highlight_address: |
| + f.write("<tr class=\"highlight-line\">\n") |
| + address_fmt = " <td><a name=\"highlight\">%s</a></td>\n" |
| + elif (line_address < highlight_address and |
| + highlight_address < next_address + start): |
| + f.write("<tr class=\"inexact-highlight-line\">\n") |
| + address_fmt = " <td><a name=\"highlight\">%s</a></td>\n" |
| + else: |
| + f.write("<tr>\n") |
| + num_bytes = next_address - line[0] |
| + stack_slot = self.heap.stack_map.get(line_address) |
| + marker = "" |
| + if stack_slot: |
| + marker = "=>" |
| + op_offset = 3 * num_bytes - 1 |
| + |
| + code = line[1] |
| + # Compute the actual call target which the disassembler is too stupid |
| + # to figure out (it adds the call offset to the disassembly offset rather |
| + # than the absolute instruction address). |
| + if self.heap.reader.arch == MD_CPU_ARCHITECTURE_X86: |
| + if code.startswith("e8"): |
| + words = code.split() |
| + if len(words) > 6 and words[5] == "call": |
| + offset = int(words[4] + words[3] + words[2] + words[1], 16) |
| + target = (line_address + offset + 5) & 0xFFFFFFFF |
| + code = code.replace(words[6], "0x%08x" % target) |
| + # TODO(jkummerow): port this hack to ARM and x64. |
| + |
| + opcodes = code[:op_offset] |
| + code = self.annotate_disasm_addresses(code[op_offset:]) |
| + f.write(" <td>") |
| + self.output_comment_box(f, "codel-", line_address) |
| + f.write("</td>\n") |
| + f.write(address_fmt % marker) |
| + self.td_from_address(f, line_address) |
| + f.write("%s (+0x%x)</td>\n" % ( |
| + self.format_address(line_address), line[0])) |
| + f.write(" <td>: %s </td>\n" % opcodes) |
| + f.write(" <td>%s</td>\n" % code) |
| + f.write("</tr>\n") |
| + |
| + def output_comment_box(self, f, prefix, address): |
| + f.write(("<input type=\"text\" class=\"commentinput\"" |
| + "id=\"%s-address-0x%s\" onchange=\"cmt()\" value=\"%s\">\n") % ( |
| + prefix, |
| + self.reader.FormatIntPtr(address), |
| + cgi.escape(self.comments.get_comment(address)) or "")) |
| + |
| + MAX_FOUND_RESULTS = 100 |
| + |
| + def output_find_results(self, f, results): |
| + f.write("Addresses") |
| + toomany = len(results) > self.MAX_FOUND_RESULTS |
| + if toomany: |
| + f.write("(found %i results, displaying only first %i)" % ( |
| + len(results), self.MAX_FOUND_RESULTS)) |
| + f.write(": \n") |
| + results = sorted(results) |
| + results = results[:min(len(results), self.MAX_FOUND_RESULTS)] |
| + for address in results: |
| + f.write("<span %s>%s</span>\n" % ( |
| + self.comments.get_style_class_string(address), \ |
| + self.format_address(address))) |
| + if toomany: |
| + f.write("...\n") |
| + |
| + |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: just one empty line
Jarin
2014/03/26 10:54:28
Done.
|
| + def output_page_info(self, f, page_kind, page_address, my_page_address): |
| + if my_page_address == page_address and page_address != 0: |
| + f.write("Marked first %s page.\n" % page_kind) |
| + else: |
| + f.write("<span id=\"%spage\" style=\"display:none\">" % page_kind) |
| + f.write("Marked first %s page." % page_kind) |
| + f.write("</span>\n") |
| + f.write("<button onclick=\"onpage('%spage', '0x%x')\">" % ( |
| + page_kind, my_page_address)) |
| + f.write("Mark as first %s page</button>\n" % page_kind) |
| + return |
| + |
| + def output_search_res(self, f, straddress): |
| + try: |
| + self.output_header(f) |
| + f.write("<h3>Search results for %s</h3>" % straddress) |
| + |
| + address = int(straddress, 0) |
| + |
| + f.write("Comment: ") |
| + self.output_comment_box(f, "search-", address) |
| + f.write("<br>\n") |
| + |
| + page_address = address & ~self.heap.PageAlignmentMask() |
| + |
| + f.write("Page info: \n") |
| + self.output_page_info(f, "data", self.padawan.known_first_data_page, \ |
| + page_address) |
| + self.output_page_info(f, "map", self.padawan.known_first_map_page, \ |
| + page_address) |
| + self.output_page_info(f, "pointer", \ |
| + self.padawan.known_first_pointer_page, \ |
| + page_address) |
| + |
| + if not self.reader.IsValidAddress(address): |
| + f.write("<h3>The contents at address %s not found in the dump.</h3>" % \ |
| + straddress) |
| + else: |
| + # Print as words |
| + self.output_words(f, address - 8, address + 32, address, "Dump") |
| + |
| + # Print as ASCII |
| + f.write("<hr>\n") |
| + self.output_ascii(f, address, address + 256, address) |
| + |
| + # Print as code |
| + f.write("<hr>\n") |
| + self.output_disasm_range(f, address - 16, address + 16, address, True) |
| + |
| + aligned_res, unaligned_res = self.reader.FindWordList(address) |
| + |
| + if len(aligned_res) > 0: |
| + f.write("<h3>Occurrences of 0x%x at aligned addresses</h3>\n" % |
| + address) |
| + self.output_find_results(f, aligned_res) |
| + |
| + if len(unaligned_res) > 0: |
| + f.write("<h3>Occurrences of 0x%x at unaligned addresses</h3>\n" % \ |
| + address) |
| + self.output_find_results(f, unaligned_res) |
| + |
| + if len(aligned_res) + len(unaligned_res) == 0: |
| + f.write("<h3>No occurences of 0x%x found in the dump</h3>\n" % address) |
| + |
| + self.output_footer(f) |
| + |
| + except ValueError: |
| + f.write("<h3>Unrecognized address format \"%s\".</h3>" % straddress) |
| + return |
| + |
| + def output_disasm_pc(self, f): |
| + address = self.reader.ExceptionIP() |
| + if not self.reader.IsValidAddress(address): |
| + return |
| + self.output_disasm_range(f, address - 16, address + 16, address, True) |
| + |
| + |
| +WEB_DUMPS_HEADER = """ |
| +<!DOCTYPE html> |
| +<html> |
| +<head> |
| +<meta content="text/html; charset=ISO-8859-1" http-equiv="content-type"> |
| +<style media="screen" type="text/css"> |
| + |
| +.dumplist { |
| + border-collapse : collapse; |
| + border-spacing : 0px; |
| + font-family: monospace; |
| +} |
| + |
| +.dumpcomments { |
| + border : 1px solid LightGray; |
| + width : 32em; |
| +} |
| + |
| +</style> |
| + |
| +<script type="application/javascript"> |
| + |
| +var dump_str = "dump-"; |
| +var dump_len = dump_str.length; |
| + |
| +function dump_cmt() { |
| + var s = event.srcElement.id; |
| + var index = s.indexOf(dump_str); |
| + if (index >= 0) { |
| + send_dump_desc(s.substring(index + dump_len), event.srcElement.value); |
| + } |
| +} |
| + |
| +function send_dump_desc(name, desc) { |
| + xmlhttp = new XMLHttpRequest(); |
| + name = encodeURIComponent(name) |
| + desc = encodeURIComponent(desc) |
| + xmlhttp.open("GET", |
| + "setdumpdesc?dump=" + name + |
| + "&description=" + desc, true); |
| + xmlhttp.send(); |
| +} |
| + |
| +</script> |
| + |
| +<title>Dump list</title> |
| +</head> |
| + |
| +<body> |
| +""" |
| + |
| +WEB_DUMPS_FOOTER = """ |
| +</body> |
| +</html> |
| +""" |
| + |
| +DUMP_FILE_RE = re.compile(r"[-_0-9a-zA-Z][-\._0-9a-zA-Z]*\.dmp$") |
| + |
| + |
| +class InspectionWebServer(HTTPServer): |
| + def __init__(self, port_number, options, minidump_name): |
| + HTTPServer.__init__(self, ('', port_number), InspectionWebHandler) |
| + splitpath = os.path.split(minidump_name) |
| + self.dumppath = splitpath[0] |
| + self.dumpfilename = splitpath[1] |
| + self.formatter = InspectionWebFormatter(options, minidump_name, self) |
| + self.formatters = { self.dumpfilename : self.formatter } |
| + print "Initialized at path '%s'" % self.dumppath |
| + |
| + def output_dump_desc_field(self, f, name): |
| + try: |
| + descfile = open(os.path.join(self.dumppath, name + ".desc"), "r") |
| + desc = descfile.readline() |
| + descfile.close() |
| + except IOError: |
| + desc = "" |
| + f.write(("<input type=\"text\" class=\"dumpcomments\" " |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: the indentation here is particularly inconsis
Jarin
2014/03/26 10:54:28
Done.
|
| + "id=\"dump-%s\" onchange=\"dump_cmt()\" value=\"%s\">\n") % ( |
| + cgi.escape(name), |
| + desc)) |
| + |
| + def set_dump_desc(self, name, description): |
| + if not DUMP_FILE_RE.match(name): |
| + return False |
| + fname = os.path.join(self.dumppath, name) |
| + if not os.path.isfile(fname): |
| + return False |
| + fname = fname + ".desc" |
| + descfile = open(fname, "w") |
| + descfile.write(description) |
| + descfile.close() |
| + return True |
| + |
| + def get_dump_formatter(self, name): |
| + if name == None: |
| + # Get the default formatter |
| + return self.formatter |
| + else: |
| + if not DUMP_FILE_RE.match(name): |
| + raise WebParameterError("Invalid name '%s'" % name) |
| + fmt = self.formatters.get(name, None) |
| + if fmt == None: |
| + try: |
| + fmt = InspectionWebFormatter( |
| + options, os.path.join(self.dumppath, name), self) |
| + self.formatters[name] = fmt |
| + except IOError: |
| + raise WebParameterError("Could not open dump '%s'" % name) |
| + return fmt |
| + |
| + def output_dumps(self, f): |
| + f.write(WEB_DUMPS_HEADER) |
| + f.write("<h3>List of available dumps</h3>") |
| + f.write("<table class=\"dumplist\">\n") |
| + f.write("<thead><tr>") |
| + f.write("<th>Name</th>") |
| + f.write("<th>File time</th>") |
| + f.write("<th>Comment</th>") |
| + f.write("</tr></thead>") |
| + dumps_by_time = { } |
| + for fname in os.listdir(self.dumppath): |
| + if DUMP_FILE_RE.match(fname): |
| + mtime = os.stat(os.path.join(self.dumppath, fname)).st_mtime |
| + fnames = dumps_by_time.get(mtime, []) |
| + fnames.append(fname) |
| + dumps_by_time[mtime] = fnames |
| + |
| + for mtime in sorted(dumps_by_time, reverse=True): |
| + fnames = dumps_by_time[mtime] |
| + for fname in fnames: |
| + f.write("<tr>\n") |
| + f.write("<td><a href=\"summary.html?%s\">%s</td>\n" % ( |
| + (urllib.urlencode({ 'dump' : fname }), fname))) |
| + f.write("<td> ") |
| + f.write(datetime.datetime.fromtimestamp(mtime)) |
| + f.write("</td>") |
| + f.write("<td> ") |
| + self.output_dump_desc_field(f, fname) |
| + f.write("</td>") |
| + f.write("</tr>\n") |
| + f.write("</table>\n") |
| + f.write(WEB_DUMPS_FOOTER) |
| + return |
| class InspectionShell(cmd.Cmd): |
| def __init__(self, reader, heap): |
| @@ -1996,6 +3153,8 @@ if __name__ == "__main__": |
| parser = optparse.OptionParser(USAGE) |
| parser.add_option("-s", "--shell", dest="shell", action="store_true", |
| help="start an interactive inspector shell") |
| + parser.add_option("-w", "--web", dest="web", action="store_true", |
| + help="start a web page") |
|
Jakob Kummerow
2014/03/24 15:21:32
nit: s/page/server on localhost:8081/ (is it worth
Jarin
2014/03/26 10:54:28
Done (the port not configurable yet)
|
| parser.add_option("-c", "--command", dest="command", default="", |
| help="run an interactive inspector shell command and exit") |
| parser.add_option("-f", "--full", dest="full", action="store_true", |
| @@ -2014,4 +3173,14 @@ if __name__ == "__main__": |
| if len(args) != 1: |
| parser.print_help() |
| sys.exit(1) |
| - AnalyzeMinidump(options, args[0]) |
| + if options.web: |
| + try: |
| + server = InspectionWebServer(PORT_NUMBER, options, args[0]) |
| + print 'Started httpserver on port ' , PORT_NUMBER |
| + webbrowser.open('http://localhost:8081/summary.html') |
| + server.serve_forever() |
| + except KeyboardInterrupt: |
| + print '^C received, shutting down the web server' |
| + server.socket.close() |
| + else: |
| + AnalyzeMinidump(options, args[0]) |