839 lines · python
1from http.server import HTTPServer, SimpleHTTPRequestHandler2import os3import sys4from urllib.parse import urlparse, unquote5import posixpath6from io import StringIO, BytesIO7import re8import shutil9import threading10import time11import socket12import itertools13import configparser14 15import Reporter16 17###18# Various patterns matched or replaced by server.19 20kReportFileRE = re.compile("(.*/)?report-(.*)\\.html")21 22kBugKeyValueRE = re.compile("<!-- BUG([^ ]*) (.*) -->")23 24# <!-- REPORTPROBLEM file="crashes/clang_crash_ndSGF9.mi" stderr="crashes/clang_crash_ndSGF9.mi.stderr.txt" info="crashes/clang_crash_ndSGF9.mi.info" -->25 26kReportCrashEntryRE = re.compile("<!-- REPORTPROBLEM (.*?)-->")27kReportCrashEntryKeyValueRE = re.compile(' ?([^=]+)="(.*?)"')28 29kReportReplacements = []30 31# Add custom javascript.32kReportReplacements.append(33 (34 re.compile("<!-- SUMMARYENDHEAD -->"),35 """\36<script language="javascript" type="text/javascript">37function load(url) {38 if (window.XMLHttpRequest) {39 req = new XMLHttpRequest();40 } else if (window.ActiveXObject) {41 req = new ActiveXObject("Microsoft.XMLHTTP");42 }43 if (req != undefined) {44 req.open("GET", url, true);45 req.send("");46 }47}48</script>""",49 )50)51 52# Insert additional columns.53kReportReplacements.append((re.compile("<!-- REPORTBUGCOL -->"), "<td></td><td></td>"))54 55# Insert report bug and open file links.56kReportReplacements.append(57 (58 re.compile('<!-- REPORTBUG id="report-(.*)\\.html" -->'),59 (60 '<td class="Button"><a href="report/\\1">Report Bug</a></td>'61 + '<td class="Button"><a href="javascript:load(\'open/\\1\')">Open File</a></td>'62 ),63 )64)65 66kReportReplacements.append(67 (68 re.compile("<!-- REPORTHEADER -->"),69 '<h3><a href="/">Summary</a> > Report %(report)s</h3>',70 )71)72 73kReportReplacements.append(74 (75 re.compile("<!-- REPORTSUMMARYEXTRA -->"),76 '<td class="Button"><a href="report/%(report)s">Report Bug</a></td>',77 )78)79 80# Insert report crashes link.81 82# Disabled for the time being until we decide exactly when this should83# be enabled. Also the radar reporter needs to be fixed to report84# multiple files.85 86# kReportReplacements.append((re.compile('<!-- REPORTCRASHES -->'),87# '<br>These files will automatically be attached to ' +88# 'reports filed here: <a href="report_crashes">Report Crashes</a>.'))89 90###91# Other simple parameters92 93kShare = posixpath.join(posixpath.dirname(__file__), "../share/scan-view")94kConfigPath = os.path.expanduser("~/.scanview.cfg")95 96###97 98__version__ = "0.1"99 100__all__ = ["create_server"]101 102 103class ReporterThread(threading.Thread):104 def __init__(self, report, reporter, parameters, server):105 threading.Thread.__init__(self)106 self.report = report107 self.server = server108 self.reporter = reporter109 self.parameters = parameters110 self.success = False111 self.status = None112 113 def run(self):114 result = None115 try:116 if self.server.options.debug:117 print("%s: SERVER: submitting bug." % (sys.argv[0],), file=sys.stderr)118 self.status = self.reporter.fileReport(self.report, self.parameters)119 self.success = True120 time.sleep(3)121 if self.server.options.debug:122 print(123 "%s: SERVER: submission complete." % (sys.argv[0],), file=sys.stderr124 )125 except Reporter.ReportFailure as e:126 self.status = e.value127 except Exception as e:128 s = StringIO()129 import traceback130 131 print("<b>Unhandled Exception</b><br><pre>", file=s)132 traceback.print_exc(file=s)133 print("</pre>", file=s)134 self.status = s.getvalue()135 136 137class ScanViewServer(HTTPServer):138 def __init__(self, address, handler, root, reporters, options):139 HTTPServer.__init__(self, address, handler)140 self.root = root141 self.reporters = reporters142 self.options = options143 self.halted = False144 self.config = None145 self.load_config()146 147 def load_config(self):148 self.config = configparser.RawConfigParser()149 150 # Add defaults151 self.config.add_section("ScanView")152 for r in self.reporters:153 self.config.add_section(r.getName())154 for p in r.getParameters():155 if p.saveConfigValue():156 self.config.set(r.getName(), p.getName(), "")157 158 # Ignore parse errors159 try:160 self.config.read([kConfigPath])161 except:162 pass163 164 # Save on exit165 import atexit166 167 atexit.register(lambda: self.save_config())168 169 def save_config(self):170 # Ignore errors (only called on exit).171 try:172 f = open(kConfigPath, "w")173 self.config.write(f)174 f.close()175 except:176 pass177 178 def halt(self):179 self.halted = True180 if self.options.debug:181 print("%s: SERVER: halting." % (sys.argv[0],), file=sys.stderr)182 183 def serve_forever(self):184 while not self.halted:185 if self.options.debug > 1:186 print("%s: SERVER: waiting..." % (sys.argv[0],), file=sys.stderr)187 try:188 self.handle_request()189 except OSError as e:190 print("OSError", e.errno)191 192 def finish_request(self, request, client_address):193 if self.options.autoReload:194 import ScanView195 196 self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler197 HTTPServer.finish_request(self, request, client_address)198 199 def handle_error(self, request, client_address):200 # Ignore socket errors201 info = sys.exc_info()202 if info and isinstance(info[1], socket.error):203 if self.options.debug > 1:204 print(205 "%s: SERVER: ignored socket error." % (sys.argv[0],),206 file=sys.stderr,207 )208 return209 HTTPServer.handle_error(self, request, client_address)210 211 212# Borrowed from Quixote, with simplifications.213def parse_query(qs, fields=None):214 if fields is None:215 fields = {}216 for chunk in (_f for _f in qs.split("&") if _f):217 if "=" not in chunk:218 name = chunk219 value = ""220 else:221 name, value = chunk.split("=", 1)222 name = unquote(name.replace("+", " "))223 value = unquote(value.replace("+", " "))224 item = fields.get(name)225 if item is None:226 fields[name] = [value]227 else:228 item.append(value)229 return fields230 231 232class ScanViewRequestHandler(SimpleHTTPRequestHandler):233 server_version = "ScanViewServer/" + __version__234 dynamic_mtime = time.time()235 236 def do_HEAD(self):237 try:238 SimpleHTTPRequestHandler.do_HEAD(self)239 except Exception as e:240 self.handle_exception(e)241 242 def do_GET(self):243 try:244 SimpleHTTPRequestHandler.do_GET(self)245 except Exception as e:246 self.handle_exception(e)247 248 def do_POST(self):249 """Serve a POST request."""250 try:251 length = self.headers.getheader("content-length") or "0"252 try:253 length = int(length)254 except:255 length = 0256 content = self.rfile.read(length)257 fields = parse_query(content)258 f = self.send_head(fields)259 if f:260 self.copyfile(f, self.wfile)261 f.close()262 except Exception as e:263 self.handle_exception(e)264 265 def log_message(self, format, *args):266 if self.server.options.debug:267 sys.stderr.write(268 "%s: SERVER: %s - - [%s] %s\n"269 % (270 sys.argv[0],271 self.address_string(),272 self.log_date_time_string(),273 format % args,274 )275 )276 277 def load_report(self, report):278 path = os.path.join(self.server.root, "report-%s.html" % report)279 data = open(path).read()280 keys = {}281 for item in kBugKeyValueRE.finditer(data):282 k, v = item.groups()283 keys[k] = v284 return keys285 286 def load_crashes(self):287 path = posixpath.join(self.server.root, "index.html")288 data = open(path).read()289 problems = []290 for item in kReportCrashEntryRE.finditer(data):291 fieldData = item.group(1)292 fields = dict(293 [i.groups() for i in kReportCrashEntryKeyValueRE.finditer(fieldData)]294 )295 problems.append(fields)296 return problems297 298 def handle_exception(self, exc):299 import traceback300 301 s = StringIO()302 print("INTERNAL ERROR\n", file=s)303 traceback.print_exc(file=s)304 f = self.send_string(s.getvalue(), "text/plain")305 if f:306 self.copyfile(f, self.wfile)307 f.close()308 309 def get_scalar_field(self, name):310 if name in self.fields:311 return self.fields[name][0]312 else:313 return None314 315 def submit_bug(self, c):316 title = self.get_scalar_field("title")317 description = self.get_scalar_field("description")318 report = self.get_scalar_field("report")319 reporterIndex = self.get_scalar_field("reporter")320 files = []321 for fileID in self.fields.get("files", []):322 try:323 i = int(fileID)324 except:325 i = None326 if i is None or i < 0 or i >= len(c.files):327 return (False, "Invalid file ID")328 files.append(c.files[i])329 330 if not title:331 return (False, "Missing title.")332 if not description:333 return (False, "Missing description.")334 try:335 reporterIndex = int(reporterIndex)336 except:337 return (False, "Invalid report method.")338 339 # Get the reporter and parameters.340 reporter = self.server.reporters[reporterIndex]341 parameters = {}342 for o in reporter.getParameters():343 name = "%s_%s" % (reporter.getName(), o.getName())344 if name not in self.fields:345 return (346 False,347 'Missing field "%s" for %s report method.'348 % (name, reporter.getName()),349 )350 parameters[o.getName()] = self.get_scalar_field(name)351 352 # Update config defaults.353 if report != "None":354 self.server.config.set("ScanView", "reporter", reporterIndex)355 for o in reporter.getParameters():356 if o.saveConfigValue():357 name = o.getName()358 self.server.config.set(reporter.getName(), name, parameters[name])359 360 # Create the report.361 bug = Reporter.BugReport(title, description, files)362 363 # Kick off a reporting thread.364 t = ReporterThread(bug, reporter, parameters, self.server)365 t.start()366 367 # Wait for thread to die...368 while t.isAlive():369 time.sleep(0.25)370 submitStatus = t.status371 372 return (t.success, t.status)373 374 def send_report_submit(self):375 report = self.get_scalar_field("report")376 c = self.get_report_context(report)377 if c.reportSource is None:378 reportingFor = "Report Crashes > "379 fileBug = (380 """\381<a href="/report_crashes">File Bug</a> > """382 % locals()383 )384 else:385 reportingFor = '<a href="/%s">Report %s</a> > ' % (c.reportSource, report)386 fileBug = '<a href="/report/%s">File Bug</a> > ' % report387 title = self.get_scalar_field("title")388 description = self.get_scalar_field("description")389 390 res, message = self.submit_bug(c)391 392 if res:393 statusClass = "SubmitOk"394 statusName = "Succeeded"395 else:396 statusClass = "SubmitFail"397 statusName = "Failed"398 399 result = (400 """401<head>402 <title>Bug Submission</title>403 <link rel="stylesheet" type="text/css" href="/scanview.css" />404</head>405<body>406<h3>407<a href="/">Summary</a> > 408%(reportingFor)s409%(fileBug)s410Submit</h3>411<form name="form" action="">412<table class="form">413<tr><td>414<table class="form_group">415<tr>416 <td class="form_clabel">Title:</td>417 <td class="form_value">418 <input type="text" name="title" size="50" value="%(title)s" disabled>419 </td>420</tr>421<tr>422 <td class="form_label">Description:</td>423 <td class="form_value">424<textarea rows="10" cols="80" name="description" disabled>425%(description)s426</textarea>427 </td>428</table>429</td></tr>430</table>431</form>432<h1 class="%(statusClass)s">Submission %(statusName)s</h1>433%(message)s434<p>435<hr>436<a href="/">Return to Summary</a>437</body>438</html>"""439 % locals()440 )441 return self.send_string(result)442 443 def send_open_report(self, report):444 try:445 keys = self.load_report(report)446 except IOError:447 return self.send_error(400, "Invalid report.")448 449 file = keys.get("FILE")450 if not file or not posixpath.exists(file):451 return self.send_error(400, 'File does not exist: "%s"' % file)452 453 import startfile454 455 if self.server.options.debug:456 print('%s: SERVER: opening "%s"' % (sys.argv[0], file), file=sys.stderr)457 458 status = startfile.open(file)459 if status:460 res = 'Opened: "%s"' % file461 else:462 res = 'Open failed: "%s"' % file463 464 return self.send_string(res, "text/plain")465 466 def get_report_context(self, report):467 class Context(object):468 pass469 470 if report is None or report == "None":471 data = self.load_crashes()472 # Don't allow empty reports.473 if not data:474 raise ValueError("No crashes detected!")475 c = Context()476 c.title = "clang static analyzer failures"477 478 stderrSummary = ""479 for item in data:480 if "stderr" in item:481 path = posixpath.join(self.server.root, item["stderr"])482 if os.path.exists(path):483 lns = itertools.islice(open(path), 0, 10)484 stderrSummary += "%s\n--\n%s" % (485 item.get("src", "<unknown>"),486 "".join(lns),487 )488 489 c.description = """\490The clang static analyzer failed on these inputs:491%s492 493STDERR Summary494--------------495%s496""" % (497 "\n".join([item.get("src", "<unknown>") for item in data]),498 stderrSummary,499 )500 c.reportSource = None501 c.navMarkup = "Report Crashes > "502 c.files = []503 for item in data:504 c.files.append(item.get("src", ""))505 c.files.append(posixpath.join(self.server.root, item.get("file", "")))506 c.files.append(507 posixpath.join(self.server.root, item.get("clangfile", ""))508 )509 c.files.append(posixpath.join(self.server.root, item.get("stderr", "")))510 c.files.append(posixpath.join(self.server.root, item.get("info", "")))511 # Just in case something failed, ignore files which don't512 # exist.513 c.files = [f for f in c.files if os.path.exists(f) and os.path.isfile(f)]514 else:515 # Check that this is a valid report.516 path = posixpath.join(self.server.root, "report-%s.html" % report)517 if not posixpath.exists(path):518 raise ValueError("Invalid report ID")519 keys = self.load_report(report)520 c = Context()521 c.title = keys.get("DESC", "clang error (unrecognized")522 c.description = """\523Bug reported by the clang static analyzer.524 525Description: %s526File: %s527Line: %s528""" % (529 c.title,530 keys.get("FILE", "<unknown>"),531 keys.get("LINE", "<unknown>"),532 )533 c.reportSource = "report-%s.html" % report534 c.navMarkup = """<a href="/%s">Report %s</a> > """ % (535 c.reportSource,536 report,537 )538 539 c.files = [path]540 return c541 542 def send_report(self, report, configOverrides=None):543 def getConfigOption(section, field):544 if (545 configOverrides is not None546 and section in configOverrides547 and field in configOverrides[section]548 ):549 return configOverrides[section][field]550 return self.server.config.get(section, field)551 552 # report is None is used for crashes553 try:554 c = self.get_report_context(report)555 except ValueError as e:556 return self.send_error(400, e.message)557 558 title = c.title559 description = c.description560 reportingFor = c.navMarkup561 if c.reportSource is None:562 extraIFrame = ""563 else:564 extraIFrame = """\565<iframe src="/%s" width="100%%" height="40%%"566 scrolling="auto" frameborder="1">567 <a href="/%s">View Bug Report</a>568</iframe>""" % (569 c.reportSource,570 c.reportSource,571 )572 573 reporterSelections = []574 reporterOptions = []575 576 try:577 active = int(getConfigOption("ScanView", "reporter"))578 except:579 active = 0580 for i, r in enumerate(self.server.reporters):581 selected = i == active582 if selected:583 selectedStr = " selected"584 else:585 selectedStr = ""586 reporterSelections.append(587 '<option value="%d"%s>%s</option>' % (i, selectedStr, r.getName())588 )589 options = "\n".join(590 [o.getHTML(r, title, getConfigOption) for o in r.getParameters()]591 )592 display = ("none", "")[selected]593 reporterOptions.append(594 """\595<tr id="%sReporterOptions" style="display:%s">596 <td class="form_label">%s Options</td>597 <td class="form_value">598 <table class="form_inner_group">599%s600 </table>601 </td>602</tr>603"""604 % (r.getName(), display, r.getName(), options)605 )606 reporterSelections = "\n".join(reporterSelections)607 reporterOptionsDivs = "\n".join(reporterOptions)608 reportersArray = "[%s]" % (609 ",".join([repr(r.getName()) for r in self.server.reporters])610 )611 612 if c.files:613 fieldSize = min(5, len(c.files))614 attachFileOptions = "\n".join(615 [616 """\617<option value="%d" selected>%s</option>"""618 % (i, v)619 for i, v in enumerate(c.files)620 ]621 )622 attachFileRow = """\623<tr>624 <td class="form_label">Attach:</td>625 <td class="form_value">626<select style="width:100%%" name="files" multiple size=%d>627%s628</select>629 </td>630</tr>631""" % (632 min(5, len(c.files)),633 attachFileOptions,634 )635 else:636 attachFileRow = ""637 638 result = (639 """<html>640<head>641 <title>File Bug</title>642 <link rel="stylesheet" type="text/css" href="/scanview.css" />643</head>644<script language="javascript" type="text/javascript">645var reporters = %(reportersArray)s;646function updateReporterOptions() {647 index = document.getElementById('reporter').selectedIndex;648 for (var i=0; i < reporters.length; ++i) {649 o = document.getElementById(reporters[i] + "ReporterOptions");650 if (i == index) {651 o.style.display = "";652 } else {653 o.style.display = "none";654 }655 }656}657</script>658<body onLoad="updateReporterOptions()">659<h3>660<a href="/">Summary</a> > 661%(reportingFor)s662File Bug</h3>663<form name="form" action="/report_submit" method="post">664<input type="hidden" name="report" value="%(report)s">665 666<table class="form">667<tr><td>668<table class="form_group">669<tr>670 <td class="form_clabel">Title:</td>671 <td class="form_value">672 <input type="text" name="title" size="50" value="%(title)s">673 </td>674</tr>675<tr>676 <td class="form_label">Description:</td>677 <td class="form_value">678<textarea rows="10" cols="80" name="description">679%(description)s680</textarea>681 </td>682</tr>683 684%(attachFileRow)s685 686</table>687<br>688<table class="form_group">689<tr>690 <td class="form_clabel">Method:</td>691 <td class="form_value">692 <select id="reporter" name="reporter" onChange="updateReporterOptions()">693 %(reporterSelections)s694 </select>695 </td>696</tr>697%(reporterOptionsDivs)s698</table>699<br>700</td></tr>701<tr><td class="form_submit">702 <input align="right" type="submit" name="Submit" value="Submit">703</td></tr>704</table>705</form>706 707%(extraIFrame)s708 709</body>710</html>"""711 % locals()712 )713 714 return self.send_string(result)715 716 def send_head(self, fields=None):717 if self.server.options.onlyServeLocal and self.client_address[0] != "127.0.0.1":718 return self.send_error(401, "Unauthorized host.")719 720 if fields is None:721 fields = {}722 self.fields = fields723 724 o = urlparse(self.path)725 self.fields = parse_query(o.query, fields)726 path = posixpath.normpath(unquote(o.path))727 728 # Split the components and strip the root prefix.729 components = path.split("/")[1:]730 731 # Special case some top-level entries.732 if components:733 name = components[0]734 if len(components) == 2:735 if name == "report":736 return self.send_report(components[1])737 elif name == "open":738 return self.send_open_report(components[1])739 elif len(components) == 1:740 if name == "quit":741 self.server.halt()742 return self.send_string("Goodbye.", "text/plain")743 elif name == "report_submit":744 return self.send_report_submit()745 elif name == "report_crashes":746 overrides = {"ScanView": {}, "Radar": {}, "Email": {}}747 for i, r in enumerate(self.server.reporters):748 if r.getName() == "Radar":749 overrides["ScanView"]["reporter"] = i750 break751 overrides["Radar"]["Component"] = "llvm - checker"752 overrides["Radar"]["Component Version"] = "X"753 return self.send_report(None, overrides)754 elif name == "favicon.ico":755 return self.send_path(posixpath.join(kShare, "bugcatcher.ico"))756 757 # Match directory entries.758 if components[-1] == "":759 components[-1] = "index.html"760 761 relpath = "/".join(components)762 path = posixpath.join(self.server.root, relpath)763 764 if self.server.options.debug > 1:765 print(766 '%s: SERVER: sending path "%s"' % (sys.argv[0], path), file=sys.stderr767 )768 return self.send_path(path)769 770 def send_404(self):771 self.send_error(404, "File not found")772 return None773 774 def send_path(self, path):775 # If the requested path is outside the root directory, do not open it776 rel = os.path.abspath(path)777 if not rel.startswith(os.path.abspath(self.server.root)):778 return self.send_404()779 780 ctype = self.guess_type(path)781 if ctype.startswith("text/"):782 # Patch file instead783 return self.send_patched_file(path, ctype)784 else:785 mode = "rb"786 try:787 f = open(path, mode)788 except IOError:789 return self.send_404()790 return self.send_file(f, ctype)791 792 def send_file(self, f, ctype):793 # Patch files to add links, but skip binary files.794 self.send_response(200)795 self.send_header("Content-type", ctype)796 fs = os.fstat(f.fileno())797 self.send_header("Content-Length", str(fs[6]))798 self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))799 self.end_headers()800 return f801 802 def send_string(self, s, ctype="text/html", headers=True, mtime=None):803 encoded_s = s.encode("utf-8")804 if headers:805 self.send_response(200)806 self.send_header("Content-type", ctype)807 self.send_header("Content-Length", str(len(encoded_s)))808 if mtime is None:809 mtime = self.dynamic_mtime810 self.send_header("Last-Modified", self.date_time_string(mtime))811 self.end_headers()812 return BytesIO(encoded_s)813 814 def send_patched_file(self, path, ctype):815 # Allow a very limited set of variables. This is pretty gross.816 variables = {}817 variables["report"] = ""818 m = kReportFileRE.match(path)819 if m:820 variables["report"] = m.group(2)821 822 try:823 f = open(path, "rb")824 except IOError:825 return self.send_404()826 fs = os.fstat(f.fileno())827 data = f.read().decode("utf-8")828 for a, b in kReportReplacements:829 data = a.sub(b % variables, data)830 return self.send_string(data, ctype, mtime=fs.st_mtime)831 832 833def create_server(address, options, root):834 import Reporter835 836 reporters = Reporter.getReporters()837 838 return ScanViewServer(address, ScanViewRequestHandler, root, reporters, options)839