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