1import BaseHTTPServer
2import SimpleHTTPServer
3import os
4import sys
5import urllib, urlparse
6import posixpath
7import StringIO
8import re
9import shutil
10import threading
11import time
12import socket
13import itertools
14
15import Reporter
16import ConfigParser
17
18###
19# Various patterns matched or replaced by server.
20
21kReportFileRE = re.compile('(.*/)?report-(.*)\\.html')
22
23kBugKeyValueRE = 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
27kReportCrashEntryRE = re.compile('<!-- REPORTPROBLEM (.*?)-->')
28kReportCrashEntryKeyValueRE = re.compile(' ?([^=]+)="(.*?)"')
29
30kReportReplacements = []
31
32# Add custom javascript.
33kReportReplacements.append((re.compile('<!-- SUMMARYENDHEAD -->'), """\
34<script language="javascript" type="text/javascript">
35function 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.
49kReportReplacements.append((re.compile('<!-- REPORTBUGCOL -->'),
50                            '<td></td><td></td>'))
51
52# Insert report bug and open file links.
53kReportReplacements.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
57kReportReplacements.append((re.compile('<!-- REPORTHEADER -->'),
58                                       '<h3><a href="/">Summary</a> > Report %(report)s</h3>'))
59
60kReportReplacements.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
76kShare = posixpath.join(posixpath.dirname(__file__), '../share/scan-view')
77kConfigPath = os.path.expanduser('~/.scanview.cfg')
78
79###
80
81__version__ = "0.1"
82
83__all__ = ["create_server"]
84
85class 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
115class 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.
185def 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
203class 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
370Submit</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 = """\
446The clang static analyzer failed on these inputs:
447%s
448
449STDERR 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 = """\
480Bug reported by the clang static analyzer.
481
482Description: %s
483File: %s
484Line: %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">
572var reporters = %(reportersArray)s;
573function 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
589File 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(kShare,'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
759def 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)
768