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