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