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