| 1 |
"""CherryPy browser for PyConquer logs. |
|---|
| 2 |
|
|---|
| 3 |
Use PyConquer to gether logs, then use the serve() function to browse the |
|---|
| 4 |
results in a web browser. If you run this module from the command line, |
|---|
| 5 |
it will call serve() for you. |
|---|
| 6 |
""" |
|---|
| 7 |
|
|---|
| 8 |
import cgi |
|---|
| 9 |
import os |
|---|
| 10 |
import re |
|---|
| 11 |
import sys |
|---|
| 12 |
|
|---|
| 13 |
try: |
|---|
| 14 |
import cStringIO as StringIO |
|---|
| 15 |
except ImportError: |
|---|
| 16 |
import StringIO |
|---|
| 17 |
|
|---|
| 18 |
import urllib |
|---|
| 19 |
|
|---|
| 20 |
import cherrypy |
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
TEMPLATE_INDEX = """<html> |
|---|
| 24 |
<head><title>PyConquer Logs</title></head> |
|---|
| 25 |
<frameset cols='250, 1*'> |
|---|
| 26 |
<frame src='menu' /> |
|---|
| 27 |
<frame name='main' src='' /> |
|---|
| 28 |
</frameset> |
|---|
| 29 |
</html> |
|---|
| 30 |
""" |
|---|
| 31 |
|
|---|
| 32 |
TEMPLATE_MENU = """<html> |
|---|
| 33 |
<head> |
|---|
| 34 |
<title>PyConquer Menu</title> |
|---|
| 35 |
<style> |
|---|
| 36 |
body {font: 9pt Arial, serif;} |
|---|
| 37 |
</style> |
|---|
| 38 |
</head> |
|---|
| 39 |
<body> |
|---|
| 40 |
<h2>PyConquer Logs</h2> |
|---|
| 41 |
<p>Click on one of the runs below to see pyconquer data.</p> |
|---|
| 42 |
%s |
|---|
| 43 |
</body> |
|---|
| 44 |
</html> |
|---|
| 45 |
""" |
|---|
| 46 |
|
|---|
| 47 |
TEMPLATE_REPORT = r"""<html> |
|---|
| 48 |
<head> |
|---|
| 49 |
<title>%(filename)s</title> |
|---|
| 50 |
<style> |
|---|
| 51 |
h2 { margin-bottom: .25em; } |
|---|
| 52 |
div { |
|---|
| 53 |
font: 10pt Courier, monotype; |
|---|
| 54 |
margin: 0 0 0 2em; |
|---|
| 55 |
background-color: #CCCCCC; |
|---|
| 56 |
} |
|---|
| 57 |
div.calltime { float: right; } |
|---|
| 58 |
a { text-decoration: none; } |
|---|
| 59 |
</style> |
|---|
| 60 |
|
|---|
| 61 |
<script type='text/javascript'> |
|---|
| 62 |
<!-- |
|---|
| 63 |
|
|---|
| 64 |
function http() { |
|---|
| 65 |
var h; |
|---|
| 66 |
if (typeof(XMLHttpRequest) != "undefined") { |
|---|
| 67 |
h = new XMLHttpRequest(); |
|---|
| 68 |
} else { |
|---|
| 69 |
try { h = new ActiveXObject("Msxml2.XMLHTTP"); } |
|---|
| 70 |
catch (e) { |
|---|
| 71 |
try { h = new ActiveXObject("Microsoft.XMLHTTP"); } |
|---|
| 72 |
catch (E) { alert("Your browser is not supported."); } |
|---|
| 73 |
} |
|---|
| 74 |
} |
|---|
| 75 |
return h |
|---|
| 76 |
} |
|---|
| 77 |
|
|---|
| 78 |
function http_action(callback) { |
|---|
| 79 |
var h = http(); |
|---|
| 80 |
h.onreadystatechange = function() { |
|---|
| 81 |
if (h.readyState == 4) { |
|---|
| 82 |
if (h.status != 200 && h.status != 204 |
|---|
| 83 |
// Internet Explorer may return 1223 for 204 |
|---|
| 84 |
&& h.status != 1223) { |
|---|
| 85 |
alert("Error. Status = " + h.status + "\n" + h.responseText); |
|---|
| 86 |
} else { |
|---|
| 87 |
var result = ""; |
|---|
| 88 |
var ct = h.getResponseHeader("Content-Type"); |
|---|
| 89 |
if (ct.split(";")[0] == "text/xml") { |
|---|
| 90 |
try { |
|---|
| 91 |
result = h.responseXML; |
|---|
| 92 |
if (xmldoc == null) throw "No XML in response"; |
|---|
| 93 |
} catch (e) { |
|---|
| 94 |
result = h.responseText; |
|---|
| 95 |
alert(h.responseText); |
|---|
| 96 |
return; |
|---|
| 97 |
} |
|---|
| 98 |
} else { |
|---|
| 99 |
result = h.responseText; |
|---|
| 100 |
} |
|---|
| 101 |
if (callback != undefined) callback(result); |
|---|
| 102 |
} |
|---|
| 103 |
} |
|---|
| 104 |
} |
|---|
| 105 |
return h; |
|---|
| 106 |
} |
|---|
| 107 |
|
|---|
| 108 |
function show(elem, style) { |
|---|
| 109 |
if (style == undefined) style = 'inline'; |
|---|
| 110 |
if (navigator.appName == "Microsoft Internet Explorer") { |
|---|
| 111 |
elem.style.display = style; |
|---|
| 112 |
} else { |
|---|
| 113 |
if (elem.tagName && (elem.tagName.toLowerCase() == 'tr')) { |
|---|
| 114 |
elem.style.visibility = 'visible'; |
|---|
| 115 |
} else { |
|---|
| 116 |
elem.style.display = style; |
|---|
| 117 |
} |
|---|
| 118 |
} |
|---|
| 119 |
} |
|---|
| 120 |
|
|---|
| 121 |
function hide(elem) { |
|---|
| 122 |
if (navigator.appName == "Microsoft Internet Explorer") { |
|---|
| 123 |
elem.style.display = 'none'; |
|---|
| 124 |
} else { |
|---|
| 125 |
if (elem.tagName && (elem.tagName.toLowerCase() == 'tr')) { |
|---|
| 126 |
elem.style.visibility = 'collapse'; |
|---|
| 127 |
} else { |
|---|
| 128 |
elem.style.display = 'none'; |
|---|
| 129 |
} |
|---|
| 130 |
} |
|---|
| 131 |
} |
|---|
| 132 |
|
|---|
| 133 |
function toggle_branch(link) { |
|---|
| 134 |
var contents = link.parentNode.nextSibling; |
|---|
| 135 |
// Firefox has an extra #text node in-between the two DIV nodes |
|---|
| 136 |
if (contents.tagName == undefined) contents = contents.nextSibling; |
|---|
| 137 |
if (contents.innerHTML == "") { |
|---|
| 138 |
function insert_branch(xmldoc) { |
|---|
| 139 |
contents.innerHTML = xmldoc; |
|---|
| 140 |
} |
|---|
| 141 |
var h = http_action(insert_branch); |
|---|
| 142 |
h.open("GET", "callblock?filename=%(filename)s&pos=" + link.id, true); |
|---|
| 143 |
h.send(null); |
|---|
| 144 |
} else { |
|---|
| 145 |
if (contents.style.display == 'none') { |
|---|
| 146 |
show(contents, 'block'); |
|---|
| 147 |
} else { |
|---|
| 148 |
hide(contents); |
|---|
| 149 |
} |
|---|
| 150 |
} |
|---|
| 151 |
} |
|---|
| 152 |
|
|---|
| 153 |
// --> |
|---|
| 154 |
</script> |
|---|
| 155 |
</head> |
|---|
| 156 |
<body> |
|---|
| 157 |
<h2>%(filename)s</h2> |
|---|
| 158 |
<div> |
|---|
| 159 |
%(content)s |
|---|
| 160 |
</div> |
|---|
| 161 |
</body> |
|---|
| 162 |
</html> |
|---|
| 163 |
""" |
|---|
| 164 |
|
|---|
| 165 |
TEMPLATE_LINE = """<div class="%s"> |
|---|
| 166 |
<div class="calltime">%s</div> |
|---|
| 167 |
%s %s (%s:%s) |
|---|
| 168 |
</div> |
|---|
| 169 |
""" |
|---|
| 170 |
|
|---|
| 171 |
TEMPLATE_CALL = """<div class="%s"> |
|---|
| 172 |
<div class="calltime">%s</div> |
|---|
| 173 |
%s <a href='javascript:void(false)' id='%s' onclick='toggle_branch(this)'>%s</a> (%s:%s) |
|---|
| 174 |
</div> |
|---|
| 175 |
<div class="call_contents"></div> |
|---|
| 176 |
""" |
|---|
| 177 |
|
|---|
| 178 |
|
|---|
| 179 |
class Event(object): |
|---|
| 180 |
|
|---|
| 181 |
def __init__(self, line, pos): |
|---|
| 182 |
self.pos = pos |
|---|
| 183 |
self.result = None |
|---|
| 184 |
self.time = None |
|---|
| 185 |
|
|---|
| 186 |
m = re.match(r"(?P<indent>-+)(?P<msgtype>.) import " |
|---|
| 187 |
r"(?P<filename>.*)( \(\d lines\))?", line) |
|---|
| 188 |
if m is not None: |
|---|
| 189 |
indent, msgtype, module, _ = m.groups() |
|---|
| 190 |
self.func = "import" |
|---|
| 191 |
self.lineno = 0 |
|---|
| 192 |
else: |
|---|
| 193 |
m = re.match(r"(?P<indent>-+)(?P<msgtype>.) (?P<func>.*)" |
|---|
| 194 |
r" \((?P<module>.*):(?P<lineno>\d+)\)(?P<result>.*)", line) |
|---|
| 195 |
indent, msgtype, func, module, lineno, result = m.groups() |
|---|
| 196 |
if result.endswith("ms"): |
|---|
| 197 |
result, time = result.rsplit(" ", 1) |
|---|
| 198 |
self.time = float(time[:-2]) |
|---|
| 199 |
self.func = func |
|---|
| 200 |
self.lineno = lineno |
|---|
| 201 |
self.result = result or None |
|---|
| 202 |
|
|---|
| 203 |
self.indent = len(indent) |
|---|
| 204 |
self.msgtype = msgtype |
|---|
| 205 |
self.module = module |
|---|
| 206 |
|
|---|
| 207 |
|
|---|
| 208 |
class LogBrowser(object): |
|---|
| 209 |
|
|---|
| 210 |
def __init__(self, path): |
|---|
| 211 |
self.path = os.path.abspath(path) |
|---|
| 212 |
|
|---|
| 213 |
def index(self): |
|---|
| 214 |
return TEMPLATE_INDEX |
|---|
| 215 |
index.exposed = True |
|---|
| 216 |
|
|---|
| 217 |
def menu(self): |
|---|
| 218 |
runs = [f for f in os.listdir(self.path) |
|---|
| 219 |
if f.startswith("pyconquer") and f.endswith(".log")] |
|---|
| 220 |
runs.sort() |
|---|
| 221 |
return TEMPLATE_MENU % '\n'.join([ |
|---|
| 222 |
"<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i) |
|---|
| 223 |
for i in runs]) |
|---|
| 224 |
menu.exposed = True |
|---|
| 225 |
|
|---|
| 226 |
def call_content(self, filename, pos): |
|---|
| 227 |
"""Return all lines one indent depth inside the given call. |
|---|
| 228 |
|
|---|
| 229 |
pos: the 0-based index for the line where the call block begins |
|---|
| 230 |
(i.e. the line of the call itself). |
|---|
| 231 |
""" |
|---|
| 232 |
pos = int(pos) |
|---|
| 233 |
lines = open(filename, 'rb').readlines()[pos:] |
|---|
| 234 |
if not lines: |
|---|
| 235 |
return [] |
|---|
| 236 |
|
|---|
| 237 |
content = [] |
|---|
| 238 |
call_line = lines.pop(0).strip() |
|---|
| 239 |
if call_line.startswith('--'): |
|---|
| 240 |
indent0 = Event(call_line, 0).indent |
|---|
| 241 |
else: |
|---|
| 242 |
indent0 = 0 |
|---|
| 243 |
|
|---|
| 244 |
for line in lines: |
|---|
| 245 |
pos += 1 |
|---|
| 246 |
line = line.strip() |
|---|
| 247 |
if line.startswith('--'): |
|---|
| 248 |
e = Event(line, pos) |
|---|
| 249 |
if e.indent <= indent0: |
|---|
| 250 |
break |
|---|
| 251 |
else: |
|---|
| 252 |
content.append(e) |
|---|
| 253 |
|
|---|
| 254 |
if not content: |
|---|
| 255 |
return [] |
|---|
| 256 |
|
|---|
| 257 |
min_depth = min([event.indent for event in content]) |
|---|
| 258 |
return [event for event in content if event.indent == min_depth] |
|---|
| 259 |
|
|---|
| 260 |
def callblock(self, filename, pos): |
|---|
| 261 |
fullpath = os.path.abspath(os.path.join(self.path, filename)) |
|---|
| 262 |
if not fullpath.startswith(self.path): |
|---|
| 263 |
raise cherrypy.HTTPError(403) |
|---|
| 264 |
|
|---|
| 265 |
results = [] |
|---|
| 266 |
for e in self.call_content(fullpath, pos): |
|---|
| 267 |
typename = {'>': 'call', '<': 'return', |
|---|
| 268 |
'[': 'ccall', ']': 'creturn', |
|---|
| 269 |
'E': 'exception', 'e': 'cexception', |
|---|
| 270 |
'.': 'line', '=': 'watch', |
|---|
| 271 |
'X': 'threadexit', '*': 'threadstart'}[e.msgtype] |
|---|
| 272 |
|
|---|
| 273 |
if e.time is None: |
|---|
| 274 |
time = '' |
|---|
| 275 |
else: |
|---|
| 276 |
time = "%0.3f" % e.time |
|---|
| 277 |
if typename in ('exception', 'cexception', 'call', 'ccall'): |
|---|
| 278 |
results.append(TEMPLATE_CALL % |
|---|
| 279 |
(typename, time, e.msgtype, e.pos, |
|---|
| 280 |
e.func, e.module, e.lineno)) |
|---|
| 281 |
else: |
|---|
| 282 |
results.append(TEMPLATE_LINE % |
|---|
| 283 |
(typename, time, e.msgtype, |
|---|
| 284 |
e.func, e.module, e.lineno)) |
|---|
| 285 |
|
|---|
| 286 |
return ''.join(results) |
|---|
| 287 |
callblock.exposed = True |
|---|
| 288 |
|
|---|
| 289 |
def report(self, filename): |
|---|
| 290 |
return TEMPLATE_REPORT % {'filename': filename, |
|---|
| 291 |
'content': self.callblock(filename, 0)} |
|---|
| 292 |
report.exposed = True |
|---|
| 293 |
|
|---|
| 294 |
|
|---|
| 295 |
def serve(port=8080, path=os.getcwd()): |
|---|
| 296 |
import cherrypy |
|---|
| 297 |
cherrypy.config.update({'server.socket_port': int(port), |
|---|
| 298 |
'server.thread_pool': 10, |
|---|
| 299 |
}) |
|---|
| 300 |
cherrypy.quickstart(LogBrowser(path)) |
|---|
| 301 |
|
|---|
| 302 |
if __name__ == "__main__": |
|---|
| 303 |
serve(*tuple(sys.argv[1:])) |
|---|
| 304 |
|
|---|