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