blob: a89bf3f24fc5a49afa370e5a78c57cdc68a612bb [file] [log] [blame]
from __future__ import print_function
try:
from http.server import HTTPServer, SimpleHTTPRequestHandler
except ImportError:
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
import os
import sys
try:
from urlparse import urlparse
from urllib import unquote
except ImportError:
from urllib.parse import urlparse, unquote
import posixpath
if sys.version_info.major >= 3:
from io import StringIO, BytesIO
else:
from io import BytesIO, BytesIO as StringIO
import re
import shutil
import threading
import time
import socket
import itertools
import Reporter
try:
import configparser
except ImportError:
import ConfigParser as 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
kShare = posixpath.join(posixpath.dirname(__file__), "../share/scan-view")
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("%s: SERVER: submitting bug." % (sys.argv[0],), file=sys.stderr)
self.status = self.reporter.fileReport(self.report, self.parameters)
self.success = True
time.sleep(3)
if self.server.options.debug:
print(
"%s: SERVER: submission complete." % (sys.argv[0],), file=sys.stderr
)
except Reporter.ReportFailure as e:
self.status = e.value
except Exception as e:
s = StringIO()
import traceback
print("<b>Unhandled Exception</b><br><pre>", file=s)
traceback.print_exc(file=s)
print("</pre>", file=s)
self.status = s.getvalue()
class ScanViewServer(HTTPServer):
def __init__(self, address, handler, root, reporters, options):
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("%s: SERVER: halting." % (sys.argv[0],), file=sys.stderr)
def serve_forever(self):
while not self.halted:
if self.options.debug > 1:
print("%s: SERVER: waiting..." % (sys.argv[0],), file=sys.stderr)
try:
self.handle_request()
except OSError as e:
print("OSError", e.errno)
def finish_request(self, request, client_address):
if self.options.autoReload:
import ScanView
self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler
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(
"%s: SERVER: ignored socket error." % (sys.argv[0],),
file=sys.stderr,
)
return
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 (_f for _f in qs.split("&") if _f):
if "=" not in chunk:
name = chunk
value = ""
else:
name, value = chunk.split("=", 1)
name = unquote(name.replace("+", " "))
value = unquote(value.replace("+", " "))
item = fields.get(name)
if item is None:
fields[name] = [value]
else:
item.append(value)
return fields
class ScanViewRequestHandler(SimpleHTTPRequestHandler):
server_version = "ScanViewServer/" + __version__
dynamic_mtime = time.time()
def do_HEAD(self):
try:
SimpleHTTPRequestHandler.do_HEAD(self)
except Exception as e:
self.handle_exception(e)
def do_GET(self):
try:
SimpleHTTPRequestHandler.do_GET(self)
except Exception as 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 as 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()
print("INTERNAL ERROR\n", file=s)
traceback.print_exc(file=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(0.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('%s: SERVER: opening "%s"' % (sys.argv[0], file), file=sys.stderr)
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(object):
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 as 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([repr(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(self.path)
self.fields = parse_query(o.query, fields)
path = posixpath.normpath(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(kShare, "bugcatcher.ico"))
# Match directory entries.
if components[-1] == "":
components[-1] = "index.html"
relpath = "/".join(components)
path = posixpath.join(self.server.root, relpath)
if self.server.options.debug > 1:
print(
'%s: SERVER: sending path "%s"' % (sys.argv[0], path), file=sys.stderr
)
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(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):
encoded_s = s.encode("utf-8")
if headers:
self.send_response(200)
self.send_header("Content-type", ctype)
self.send_header("Content-Length", str(len(encoded_s)))
if mtime is None:
mtime = self.dynamic_mtime
self.send_header("Last-Modified", self.date_time_string(mtime))
self.end_headers()
return BytesIO(encoded_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, "rb")
except IOError:
return self.send_404()
fs = os.fstat(f.fileno())
data = f.read().decode("utf-8")
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)