| 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): |
| 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) |