blob: 32570b985838497cc887d4e113b44a38c461cdd9 [file] [log] [blame]
import BaseHTTPServer
import SimpleHTTPServer
import os
import sys
import urllib, urlparse
import posixpath
import StringIO
import re
import shutil
import threading
import time
import socket
import itertools
import Reporter
import ConfigParser
###
# Various patterns matched or replaced by server.
kReportFileRE = re.compile('(.*/)?report-(.*)\\.html')
kBugKeyValueRE = re.compile('<!-- BUG([^ ]*) (.*) -->')
# <!-- REPORTPROBLEM file="crashes/clang_crash_ndSGF9.mi" stderr="crashes/clang_crash_ndSGF9.mi.stderr.txt" info="crashes/clang_crash_ndSGF9.mi.info" -->
kReportCrashEntryRE = re.compile('<!-- REPORTPROBLEM (.*?)-->')
kReportCrashEntryKeyValueRE = re.compile(' ?([^=]+)="(.*?)"')
kReportReplacements = []
# Add custom javascript.
kReportReplacements.append((re.compile('<!-- SUMMARYENDHEAD -->'), """\
<script language="javascript" type="text/javascript">
function load(url) {
if (window.XMLHttpRequest) {
req = new XMLHttpRequest();
} else if (window.ActiveXObject) {
req = new ActiveXObject("Microsoft.XMLHTTP");
}
if (req != undefined) {
req.open("GET", url, true);
req.send("");
}
}
</script>"""))
# Insert additional columns.
kReportReplacements.append((re.compile('<!-- REPORTBUGCOL -->'),
'<td></td><td></td>'))
# Insert report bug and open file links.
kReportReplacements.append((re.compile('<!-- REPORTBUG id="report-(.*)\\.html" -->'),
('<td class="Button"><a href="report/\\1">Report Bug</a></td>' +
'<td class="Button"><a href="javascript:load(\'open/\\1\')">Open File</a></td>')))
kReportReplacements.append((re.compile('<!-- REPORTHEADER -->'),
'<h3><a href="/">Summary</a> > Report %(report)s</h3>'))
kReportReplacements.append((re.compile('<!-- REPORTSUMMARYEXTRA -->'),
'<td class="Button"><a href="report/%(report)s">Report Bug</a></td>'))
# Insert report crashes link.
# Disabled for the time being until we decide exactly when this should
# be enabled. Also the radar reporter needs to be fixed to report
# multiple files.
#kReportReplacements.append((re.compile('<!-- REPORTCRASHES -->'),
# '<br>These files will automatically be attached to ' +
# 'reports filed here: <a href="report_crashes">Report Crashes</a>.'))
###
# Other simple parameters
kResources = posixpath.join(posixpath.dirname(__file__), 'Resources')
kConfigPath = os.path.expanduser('~/.scanview.cfg')
###
__version__ = "0.1"
__all__ = ["create_server"]
class ReporterThread(threading.Thread):
def __init__(self, report, reporter, parameters, server):
threading.Thread.__init__(self)
self.report = report
self.server = server
self.reporter = reporter
self.parameters = parameters
self.success = False
self.status = None
def run(self):
result = None
try:
if self.server.options.debug:
print >>sys.stderr, "%s: SERVER: submitting bug."%(sys.argv[0],)
self.status = self.reporter.fileReport(self.report, self.parameters)
self.success = True
time.sleep(3)
if self.server.options.debug:
print >>sys.stderr, "%s: SERVER: submission complete."%(sys.argv[0],)
except Reporter.ReportFailure,e:
self.status = e.value
except Exception,e:
s = StringIO.StringIO()
import traceback
print >>s,'<b>Unhandled Exception</b><br><pre>'
traceback.print_exc(e,file=s)
print >>s,'</pre>'
self.status = s.getvalue()
class ScanViewServer(BaseHTTPServer.HTTPServer):
def __init__(self, address, handler, root, reporters, options):
BaseHTTPServer.HTTPServer.__init__(self, address, handler)
self.root = root
self.reporters = reporters
self.options = options
self.halted = False
self.config = None
self.load_config()
def load_config(self):
self.config = ConfigParser.RawConfigParser()
# Add defaults
self.config.add_section('ScanView')
for r in self.reporters:
self.config.add_section(r.getName())
for p in r.getParameters():
if p.saveConfigValue():
self.config.set(r.getName(), p.getName(), '')
# Ignore parse errors
try:
self.config.read([kConfigPath])
except:
pass
# Save on exit
import atexit
atexit.register(lambda: self.save_config())
def save_config(self):
# Ignore errors (only called on exit).
try:
f = open(kConfigPath,'w')
self.config.write(f)
f.close()
except:
pass
def halt(self):
self.halted = True
if self.options.debug:
print >>sys.stderr, "%s: SERVER: halting." % (sys.argv[0],)
def serve_forever(self):
while not self.halted:
if self.options.debug > 1:
print >>sys.stderr, "%s: SERVER: waiting..." % (sys.argv[0],)
try:
self.handle_request()
except OSError,e:
print 'OSError',e.errno
def finish_request(self, request, client_address):
if self.options.autoReload:
import ScanView
self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler
BaseHTTPServer.HTTPServer.finish_request(self, request, client_address)
def handle_error(self, request, client_address):
# Ignore socket errors
info = sys.exc_info()
if info and isinstance(info[1], socket.error):
if self.options.debug > 1:
print >>sys.stderr, "%s: SERVER: ignored socket error." % (sys.argv[0],)
return
BaseHTTPServer.HTTPServer.handle_error(self, request, client_address)
# Borrowed from Quixote, with simplifications.
def parse_query(qs, fields=None):
if fields is None:
fields = {}
for chunk in filter(None, qs.split('&')):
if '=' not in chunk:
name = chunk
value = ''
else:
name, value = chunk.split('=', 1)
name = urllib.unquote(name.replace('+', ' '))
value = urllib.unquote(value.replace('+', ' '))
item = fields.get(name)
if item is None:
fields[name] = [value]
else:
item.append(value)
return fields
class ScanViewRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
server_version = "ScanViewServer/" + __version__
dynamic_mtime = time.time()
def do_HEAD(self):
try:
SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self)
except Exception,e:
self.handle_exception(e)
def do_GET(self):
try:
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
except Exception,e:
self.handle_exception(e)
def do_POST(self):
"""Serve a POST request."""
try:
length = self.headers.getheader('content-length') or "0"
try:
length = int(length)
except:
length = 0
content = self.rfile.read(length)
fields = parse_query(content)
f = self.send_head(fields)
if f:
self.copyfile(f, self.wfile)
f.close()
except Exception,e:
self.handle_exception(e)
def log_message(self, format, *args):
if self.server.options.debug:
sys.stderr.write("%s: SERVER: %s - - [%s] %s\n" %
(sys.argv[0],
self.address_string(),
self.log_date_time_string(),
format%args))
def load_report(self, report):
path = os.path.join(self.server.root, 'report-%s.html'%report)
data = open(path).read()
keys = {}
for item in kBugKeyValueRE.finditer(data):
k,v = item.groups()
keys[k] = v
return keys
def load_crashes(self):
path = posixpath.join(self.server.root, 'index.html')
data = open(path).read()
problems = []
for item in kReportCrashEntryRE.finditer(data):
fieldData = item.group(1)
fields = dict([i.groups() for i in
kReportCrashEntryKeyValueRE.finditer(fieldData)])
problems.append(fields)
return problems
def handle_exception(self, exc):
import traceback
s = StringIO.StringIO()
print >>s, "INTERNAL ERROR\n"
traceback.print_exc(exc, s)
f = self.send_string(s.getvalue(), 'text/plain')
if f:
self.copyfile(f, self.wfile)
f.close()
def get_scalar_field(self, name):
if name in self.fields:
return self.fields[name][0]
else:
return None
def submit_bug(self, c):
title = self.get_scalar_field('title')
description = self.get_scalar_field('description')
report = self.get_scalar_field('report')
reporterIndex = self.get_scalar_field('reporter')
files = []
for fileID in self.fields.get('files',[]):
try:
i = int(fileID)
except:
i = None
if i is None or i<0 or i>=len(c.files):
return (False, 'Invalid file ID')
files.append(c.files[i])
if not title:
return (False, "Missing title.")
if not description:
return (False, "Missing description.")
try:
reporterIndex = int(reporterIndex)
except:
return (False, "Invalid report method.")
# Get the reporter and parameters.
reporter = self.server.reporters[reporterIndex]
parameters = {}
for o in reporter.getParameters():
name = '%s_%s'%(reporter.getName(),o.getName())
if name not in self.fields:
return (False,
'Missing field "%s" for %s report method.'%(name,
reporter.getName()))
parameters[o.getName()] = self.get_scalar_field(name)
# Update config defaults.
if report != 'None':
self.server.config.set('ScanView', 'reporter', reporterIndex)
for o in reporter.getParameters():
if o.saveConfigValue():
name = o.getName()
self.server.config.set(reporter.getName(), name, parameters[name])
# Create the report.
bug = Reporter.BugReport(title, description, files)
# Kick off a reporting thread.
t = ReporterThread(bug, reporter, parameters, self.server)
t.start()
# Wait for thread to die...
while t.isAlive():
time.sleep(.25)
submitStatus = t.status
return (t.success, t.status)
def send_report_submit(self):
report = self.get_scalar_field('report')
c = self.get_report_context(report)
if c.reportSource is None:
reportingFor = "Report Crashes > "
fileBug = """\
<a href="/report_crashes">File Bug</a> > """%locals()
else:
reportingFor = '<a href="/%s">Report %s</a> > ' % (c.reportSource,
report)
fileBug = '<a href="/report/%s">File Bug</a> > ' % report
title = self.get_scalar_field('title')
description = self.get_scalar_field('description')
res,message = self.submit_bug(c)
if res:
statusClass = 'SubmitOk'
statusName = 'Succeeded'
else:
statusClass = 'SubmitFail'
statusName = 'Failed'
result = """
<head>
<title>Bug Submission</title>
<link rel="stylesheet" type="text/css" href="/scanview.css" />
</head>
<body>
<h3>
<a href="/">Summary</a> >
%(reportingFor)s
%(fileBug)s
Submit</h3>
<form name="form" action="">
<table class="form">
<tr><td>
<table class="form_group">
<tr>
<td class="form_clabel">Title:</td>
<td class="form_value">
<input type="text" name="title" size="50" value="%(title)s" disabled>
</td>
</tr>
<tr>
<td class="form_label">Description:</td>
<td class="form_value">
<textarea rows="10" cols="80" name="description" disabled>
%(description)s
</textarea>
</td>
</table>
</td></tr>
</table>
</form>
<h1 class="%(statusClass)s">Submission %(statusName)s</h1>
%(message)s
<p>
<hr>
<a href="/">Return to Summary</a>
</body>
</html>"""%locals()
return self.send_string(result)
def send_open_report(self, report):
try:
keys = self.load_report(report)
except IOError:
return self.send_error(400, 'Invalid report.')
file = keys.get('FILE')
if not file or not posixpath.exists(file):
return self.send_error(400, 'File does not exist: "%s"' % file)
import startfile
if self.server.options.debug:
print >>sys.stderr, '%s: SERVER: opening "%s"'%(sys.argv[0],
file)
status = startfile.open(file)
if status:
res = 'Opened: "%s"' % file
else:
res = 'Open failed: "%s"' % file
return self.send_string(res, 'text/plain')
def get_report_context(self, report):
class Context:
pass
if report is None or report == 'None':
data = self.load_crashes()
# Don't allow empty reports.
if not data:
raise ValueError, 'No crashes detected!'
c = Context()
c.title = 'clang static analyzer failures'
stderrSummary = ""
for item in data:
if 'stderr' in item:
path = posixpath.join(self.server.root, item['stderr'])
if os.path.exists(path):
lns = itertools.islice(open(path), 0, 10)
stderrSummary += '%s\n--\n%s' % (item.get('src',
'<unknown>'),
''.join(lns))
c.description = """\
The clang static analyzer failed on these inputs:
%s
STDERR Summary
--------------
%s
""" % ('\n'.join([item.get('src','<unknown>') for item in data]),
stderrSummary)
c.reportSource = None
c.navMarkup = "Report Crashes > "
c.files = []
for item in data:
c.files.append(item.get('src',''))
c.files.append(posixpath.join(self.server.root,
item.get('file','')))
c.files.append(posixpath.join(self.server.root,
item.get('clangfile','')))
c.files.append(posixpath.join(self.server.root,
item.get('stderr','')))
c.files.append(posixpath.join(self.server.root,
item.get('info','')))
# Just in case something failed, ignore files which don't
# exist.
c.files = [f for f in c.files
if os.path.exists(f) and os.path.isfile(f)]
else:
# Check that this is a valid report.
path = posixpath.join(self.server.root, 'report-%s.html' % report)
if not posixpath.exists(path):
raise ValueError, 'Invalid report ID'
keys = self.load_report(report)
c = Context()
c.title = keys.get('DESC','clang error (unrecognized')
c.description = """\
Bug reported by the clang static analyzer.
Description: %s
File: %s
Line: %s
"""%(c.title, keys.get('FILE','<unknown>'), keys.get('LINE', '<unknown>'))
c.reportSource = 'report-%s.html' % report
c.navMarkup = """<a href="/%s">Report %s</a> > """ % (c.reportSource,
report)
c.files = [path]
return c
def send_report(self, report, configOverrides=None):
def getConfigOption(section, field):
if (configOverrides is not None and
section in configOverrides and
field in configOverrides[section]):
return configOverrides[section][field]
return self.server.config.get(section, field)
# report is None is used for crashes
try:
c = self.get_report_context(report)
except ValueError, e:
return self.send_error(400, e.message)
title = c.title
description= c.description
reportingFor = c.navMarkup
if c.reportSource is None:
extraIFrame = ""
else:
extraIFrame = """\
<iframe src="/%s" width="100%%" height="40%%"
scrolling="auto" frameborder="1">
<a href="/%s">View Bug Report</a>
</iframe>""" % (c.reportSource, c.reportSource)
reporterSelections = []
reporterOptions = []
try:
active = int(getConfigOption('ScanView','reporter'))
except:
active = 0
for i,r in enumerate(self.server.reporters):
selected = (i == active)
if selected:
selectedStr = ' selected'
else:
selectedStr = ''
reporterSelections.append('<option value="%d"%s>%s</option>'%(i,selectedStr,r.getName()))
options = '\n'.join([ o.getHTML(r,title,getConfigOption) for o in r.getParameters()])
display = ('none','')[selected]
reporterOptions.append("""\
<tr id="%sReporterOptions" style="display:%s">
<td class="form_label">%s Options</td>
<td class="form_value">
<table class="form_inner_group">
%s
</table>
</td>
</tr>
"""%(r.getName(),display,r.getName(),options))
reporterSelections = '\n'.join(reporterSelections)
reporterOptionsDivs = '\n'.join(reporterOptions)
reportersArray = '[%s]'%(','.join([`r.getName()` for r in self.server.reporters]))
if c.files:
fieldSize = min(5, len(c.files))
attachFileOptions = '\n'.join(["""\
<option value="%d" selected>%s</option>""" % (i,v) for i,v in enumerate(c.files)])
attachFileRow = """\
<tr>
<td class="form_label">Attach:</td>
<td class="form_value">
<select style="width:100%%" name="files" multiple size=%d>
%s
</select>
</td>
</tr>
""" % (min(5, len(c.files)), attachFileOptions)
else:
attachFileRow = ""
result = """<html>
<head>
<title>File Bug</title>
<link rel="stylesheet" type="text/css" href="/scanview.css" />
</head>
<script language="javascript" type="text/javascript">
var reporters = %(reportersArray)s;
function updateReporterOptions() {
index = document.getElementById('reporter').selectedIndex;
for (var i=0; i < reporters.length; ++i) {
o = document.getElementById(reporters[i] + "ReporterOptions");
if (i == index) {
o.style.display = "";
} else {
o.style.display = "none";
}
}
}
</script>
<body onLoad="updateReporterOptions()">
<h3>
<a href="/">Summary</a> >
%(reportingFor)s
File Bug</h3>
<form name="form" action="/report_submit" method="post">
<input type="hidden" name="report" value="%(report)s">
<table class="form">
<tr><td>
<table class="form_group">
<tr>
<td class="form_clabel">Title:</td>
<td class="form_value">
<input type="text" name="title" size="50" value="%(title)s">
</td>
</tr>
<tr>
<td class="form_label">Description:</td>
<td class="form_value">
<textarea rows="10" cols="80" name="description">
%(description)s
</textarea>
</td>
</tr>
%(attachFileRow)s
</table>
<br>
<table class="form_group">
<tr>
<td class="form_clabel">Method:</td>
<td class="form_value">
<select id="reporter" name="reporter" onChange="updateReporterOptions()">
%(reporterSelections)s
</select>
</td>
</tr>
%(reporterOptionsDivs)s
</table>
<br>
</td></tr>
<tr><td class="form_submit">
<input align="right" type="submit" name="Submit" value="Submit">
</td></tr>
</table>
</form>
%(extraIFrame)s
</body>
</html>"""%locals()
return self.send_string(result)
def send_head(self, fields=None):
if (self.server.options.onlyServeLocal and
self.client_address[0] != '127.0.0.1'):
return self.send_error(401, 'Unauthorized host.')
if fields is None:
fields = {}
self.fields = fields
o = urlparse.urlparse(self.path)
self.fields = parse_query(o.query, fields)
path = posixpath.normpath(urllib.unquote(o.path))
# Split the components and strip the root prefix.
components = path.split('/')[1:]
# Special case some top-level entries.
if components:
name = components[0]
if len(components)==2:
if name=='report':
return self.send_report(components[1])
elif name=='open':
return self.send_open_report(components[1])
elif len(components)==1:
if name=='quit':
self.server.halt()
return self.send_string('Goodbye.', 'text/plain')
elif name=='report_submit':
return self.send_report_submit()
elif name=='report_crashes':
overrides = { 'ScanView' : {},
'Radar' : {},
'Email' : {} }
for i,r in enumerate(self.server.reporters):
if r.getName() == 'Radar':
overrides['ScanView']['reporter'] = i
break
overrides['Radar']['Component'] = 'llvm - checker'
overrides['Radar']['Component Version'] = 'X'
return self.send_report(None, overrides)
elif name=='favicon.ico':
return self.send_path(posixpath.join(kResources,'bugcatcher.ico'))
# Match directory entries.
if components[-1] == '':
components[-1] = 'index.html'
suffix = '/'.join(components)
# The summary may reference source files on disk using rooted
# paths. Make sure these resolve correctly for now.
# FIXME: This isn't a very good idea... we should probably
# mark rooted paths somehow.
if os.path.exists(posixpath.join('/', suffix)):
path = posixpath.join('/', suffix)
else:
path = posixpath.join(self.server.root, suffix)
if self.server.options.debug > 1:
print >>sys.stderr, '%s: SERVER: sending path "%s"'%(sys.argv[0],
path)
return self.send_path(path)
def send_404(self):
self.send_error(404, "File not found")
return None
def send_path(self, path):
# If the requested path is outside the root directory, do not open it
rel = os.path.abspath(os.path.join(self.server.root, path))
if not rel.startswith(os.path.abspath(self.server.root) ):
return self.send_404()
ctype = self.guess_type(path)
if ctype.startswith('text/'):
# Patch file instead
return self.send_patched_file(path, ctype)
else:
mode = 'rb'
try:
f = open(path, mode)
except IOError:
return self.send_404()
return self.send_file(f, ctype)
def send_file(self, f, ctype):
# Patch files to add links, but skip binary files.
self.send_response(200)
self.send_header("Content-type", ctype)
fs = os.fstat(f.fileno())
self.send_header("Content-Length", str(fs[6]))
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
self.end_headers()
return f
def send_string(self, s, ctype='text/html', headers=True, mtime=None):
if headers:
self.send_response(200)
self.send_header("Content-type", ctype)
self.send_header("Content-Length", str(len(s)))
if mtime is None:
mtime = self.dynamic_mtime
self.send_header("Last-Modified", self.date_time_string(mtime))
self.end_headers()
return StringIO.StringIO(s)
def send_patched_file(self, path, ctype):
# Allow a very limited set of variables. This is pretty gross.
variables = {}
variables['report'] = ''
m = kReportFileRE.match(path)
if m:
variables['report'] = m.group(2)
try:
f = open(path,'r')
except IOError:
return self.send_404()
fs = os.fstat(f.fileno())
data = f.read()
for a,b in kReportReplacements:
data = a.sub(b % variables, data)
return self.send_string(data, ctype, mtime=fs.st_mtime)
def create_server(address, options, root):
import Reporter
reporters = Reporter.getReporters()
return ScanViewServer(address, ScanViewRequestHandler,
root,
reporters,
options)