| import BaseHTTPServer |
| import SimpleHTTPServer |
| import os |
| import sys |
| import urllib, urlparse |
| import posixpath |
| import StringIO |
| import re |
| import shutil |
| import threading |
| import time |
| import socket |
| |
| import Reporter |
| import ConfigParser |
| |
| ### |
| # Various patterns matched or replaced by server. |
| |
| kReportFileRE = re.compile('(.*/)?report-(.*)\\.html') |
| |
| kBugKeyValueRE = re.compile('<!-- BUG([^ ]*) (.*) -->') |
| |
| kReportReplacements = [] |
| |
| # Add custom javascript. |
| kReportReplacements.append((re.compile('<!-- SUMMARYENDHEAD -->'), """\ |
| <script language="javascript" type="text/javascript"> |
| function load(url) { |
| if (window.XMLHttpRequest) { |
| req = new XMLHttpRequest(); |
| } else if (window.ActiveXObject) { |
| req = new ActiveXObject("Microsoft.XMLHTTP"); |
| } |
| if (req != undefined) { |
| req.open("GET", url, true); |
| req.send(""); |
| } |
| } |
| </script>""")) |
| |
| # Insert additional columns. |
| kReportReplacements.append((re.compile('<!-- REPORTBUGCOL -->'), |
| '<td></td><td></td>')) |
| |
| # Insert report bug and open file links. |
| kReportReplacements.append((re.compile('<!-- REPORTBUG id="report-(.*)\\.html" -->'), |
| ('<td class="Button"><a href="report/\\1">Report Bug</a></td>' + |
| '<td class="Button"><a href="javascript:load(\'open/\\1\')">Open File</a></td>'))) |
| |
| kReportReplacements.append((re.compile('<!-- REPORTHEADER -->'), |
| '<h3><a href="/">Summary</a> > Report %(report)s</h3>')) |
| |
| kReportReplacements.append((re.compile('<!-- REPORTSUMMARYEXTRA -->'), |
| '<td class="Button"><a href="report/%(report)s">Report Bug</a></td>')) |
| |
| ### |
| # Other simple parameters |
| |
| kResources = posixpath.join(posixpath.dirname(__file__), 'Resources') |
| kConfigPath = os.path.expanduser('~/.scanview.cfg') |
| |
| ### |
| |
| __version__ = "0.1" |
| |
| __all__ = ["create_server"] |
| |
| class ReporterThread(threading.Thread): |
| def __init__(self, report, reporter, parameters, server): |
| threading.Thread.__init__(self) |
| self.report = report |
| self.server = server |
| self.reporter = reporter |
| self.parameters = parameters |
| self.success = False |
| self.status = None |
| |
| def run(self): |
| result = None |
| try: |
| if self.server.options.debug: |
| print >>sys.stderr, "%s: SERVER: submitting bug."%(sys.argv[0],) |
| self.status = self.reporter.fileReport(self.report, self.parameters) |
| self.success = True |
| time.sleep(3) |
| if self.server.options.debug: |
| print >>sys.stderr, "%s: SERVER: submission complete."%(sys.argv[0],) |
| except Reporter.ReportFailure,e: |
| self.status = e.value |
| except Exception,e: |
| s = StringIO.StringIO() |
| import traceback |
| print >>s,'<b>Unhandled Exception</b><br><pre>' |
| traceback.print_exc(e,file=s) |
| print >>s,'</pre>' |
| self.status = s.getvalue() |
| |
| class ScanViewServer(BaseHTTPServer.HTTPServer): |
| def __init__(self, address, handler, root, reporters, options): |
| BaseHTTPServer.HTTPServer.__init__(self, address, handler) |
| self.root = root |
| self.reporters = reporters |
| self.options = options |
| self.halted = False |
| self.config = None |
| self.load_config() |
| |
| def load_config(self): |
| self.config = ConfigParser.RawConfigParser() |
| |
| # Add defaults |
| self.config.add_section('ScanView') |
| for r in self.reporters: |
| self.config.add_section(r.getName()) |
| for p in r.getParameterNames(): |
| self.config.set(r.getName(), p, '') |
| |
| # Ignore parse errors |
| try: |
| self.config.read([kConfigPath]) |
| except: |
| pass |
| |
| # Save on exit |
| import atexit |
| atexit.register(lambda: self.save_config()) |
| |
| def save_config(self): |
| # Ignore errors (only called on exit). |
| try: |
| f = open(kConfigPath,'w') |
| self.config.write(f) |
| f.close() |
| except: |
| pass |
| |
| def halt(self): |
| self.halted = True |
| if self.options.debug: |
| print >>sys.stderr, "%s: SERVER: halting." % (sys.argv[0],) |
| |
| def serve_forever(self): |
| while not self.halted: |
| if self.options.debug > 1: |
| print >>sys.stderr, "%s: SERVER: waiting..." % (sys.argv[0],) |
| try: |
| self.handle_request() |
| except OSError,e: |
| print 'OSError',e.errno |
| |
| def finish_request(self, request, client_address): |
| if self.options.autoReload: |
| import ScanView |
| self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler |
| BaseHTTPServer.HTTPServer.finish_request(self, request, client_address) |
| |
| def handle_error(self, request, client_address): |
| # Ignore socket errors |
| info = sys.exc_info() |
| if info and isinstance(info[1], socket.error): |
| if self.options.debug > 1: |
| print >>sys.stderr, "%s: SERVER: ignored socket error." % (sys.argv[0],) |
| return |
| BaseHTTPServer.HTTPServer.handle_error(self, request, client_address) |
| |
| # Borrowed from Quixote, with simplifications. |
| def parse_query(qs, fields=None): |
| if fields is None: |
| fields = {} |
| for chunk in filter(None, qs.split('&')): |
| if '=' not in chunk: |
| name = chunk |
| value = '' |
| else: |
| name, value = chunk.split('=', 1) |
| name = urllib.unquote(name.replace('+', ' ')) |
| value = urllib.unquote(value.replace('+', ' ')) |
| fields[name] = value |
| return fields |
| |
| class ScanViewRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): |
| server_version = "ScanViewServer/" + __version__ |
| dynamic_mtime = time.time() |
| |
| def do_HEAD(self): |
| try: |
| SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self) |
| except Exception,e: |
| self.handle_exception(e) |
| |
| def do_GET(self): |
| try: |
| SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) |
| except Exception,e: |
| self.handle_exception(e) |
| |
| def do_POST(self): |
| """Serve a POST request.""" |
| try: |
| length = self.headers.getheader('content-length') or "0" |
| try: |
| length = int(length) |
| except: |
| length = 0 |
| content = self.rfile.read(length) |
| fields = parse_query(content) |
| f = self.send_head(fields) |
| if f: |
| self.copyfile(f, self.wfile) |
| f.close() |
| except Exception,e: |
| self.handle_exception(e) |
| |
| def log_message(self, format, *args): |
| if self.server.options.debug: |
| sys.stderr.write("%s: SERVER: %s - - [%s] %s\n" % |
| (sys.argv[0], |
| self.address_string(), |
| self.log_date_time_string(), |
| format%args)) |
| |
| def load_report(self, report): |
| path = os.path.join(self.server.root, 'report-%s.html'%report) |
| data = open(path).read() |
| keys = {} |
| for item in kBugKeyValueRE.finditer(data): |
| k,v = item.groups() |
| keys[k] = v |
| return keys |
| |
| def handle_exception(self, exc): |
| import traceback |
| s = StringIO.StringIO() |
| print >>s, "INTERNAL ERROR\n" |
| traceback.print_exc(exc, s) |
| f = self.send_string(s.getvalue(), 'text/plain') |
| if f: |
| self.copyfile(f, self.wfile) |
| f.close() |
| |
| def submit_bug(self): |
| title = self.fields.get('title') |
| description = self.fields.get('description') |
| report = self.fields.get('report') |
| reporterIndex = self.fields.get('reporter') |
| |
| # Type check form parameters. |
| reportPath = posixpath.join(self.server.root, |
| 'report-%s.html' % report) |
| if not posixpath.exists(reportPath): |
| return (False, "Invalid report ID.") |
| if not title: |
| return (False, "Missing title.") |
| if not description: |
| return (False, "Missing description.") |
| try: |
| reporterIndex = int(reporterIndex) |
| except: |
| return (False, "Invalid report method.") |
| |
| # Get the reporter and parameters. |
| reporter = self.server.reporters[reporterIndex] |
| parameters = {} |
| for o in reporter.getParameterNames(): |
| name = '%s_%s'%(reporter.getName(),o) |
| if name not in self.fields: |
| return (False, |
| 'Missing field "%s" for %s report method.'%(name, |
| reporter.getName())) |
| parameters[o] = self.fields[name] |
| |
| # Update config defaults. |
| self.server.config.set('ScanView', 'reporter', reporterIndex) |
| for o in reporter.getParameterNames(): |
| self.server.config.set(reporter.getName(), o, parameters[o]) |
| |
| # Create the report. |
| bug = Reporter.BugReport(title, description, [reportPath]) |
| |
| # Kick off a reporting thread. |
| t = ReporterThread(bug, reporter, parameters, self.server) |
| t.start() |
| |
| # Wait for thread to die... |
| while t.isAlive(): |
| time.sleep(.25) |
| submitStatus = t.status |
| |
| return (t.success, t.status) |
| |
| def send_report_submit(self): |
| report = self.fields.get('report') |
| title = self.fields.get('title') |
| description = self.fields.get('description') |
| |
| res,message = self.submit_bug() |
| |
| if res: |
| statusClass = 'SubmitOk' |
| statusName = 'Succeeded' |
| else: |
| statusClass = 'SubmitFail' |
| statusName = 'Failed' |
| |
| result = """ |
| <head> |
| <title>Bug Submission</title> |
| <link rel="stylesheet" type="text/css" href="/scanview.css" /> |
| </head> |
| <body> |
| <h3> |
| <a href="/">Summary</a> > |
| <a href="/report-%(report)s.html">Report %(report)s</a> > |
| <a href="/report/%(report)s">File Bug</a> > |
| Submit</h3> |
| <form name="form" action=""> |
| <table class="form"> |
| <tr><td> |
| <table class="form_group"> |
| <tr> |
| <td class="form_clabel">Title:</td> |
| <td class="form_value"> |
| <input type="text" name="title" size="50" value="%(title)s" disabled> |
| </td> |
| </tr> |
| <tr> |
| <td class="form_label">Description:</td> |
| <td class="form_value"> |
| <textarea rows="10" cols="80" name="description" disabled> |
| %(description)s |
| </textarea> |
| </td> |
| </table> |
| </td></tr> |
| </table> |
| </form> |
| <h1 class="%(statusClass)s">Submission %(statusName)s</h1> |
| %(message)s |
| <p> |
| <hr> |
| <a href="/">Return to Summary</a> |
| </body> |
| </html>"""%locals() |
| return self.send_string(result) |
| |
| def send_open_report(self, report): |
| try: |
| keys = self.load_report(report) |
| except IOError: |
| return self.send_error(400, 'Invalid report.') |
| |
| file = keys.get('FILE') |
| if not file or not posixpath.exists(file): |
| return self.send_error(400, 'File does not exist: "%s"' % file) |
| |
| import startfile |
| if self.server.options.debug: |
| print >>sys.stderr, '%s: SERVER: opening "%s"'%(sys.argv[0], |
| file) |
| |
| status = startfile.open(file) |
| if status: |
| res = 'Opened: "%s"' % file |
| else: |
| res = 'Open failed: "%s"' % file |
| |
| return self.send_string(res, 'text/plain') |
| |
| def send_report(self, report): |
| try: |
| keys = self.load_report(report) |
| except IOError: |
| return self.send_error(400, 'Invalid report.') |
| |
| initialTitle = keys.get('DESC','') |
| initialDescription = """\ |
| Bug generated by the clang static analyzer. |
| |
| Description: %s |
| File: %s |
| Line: %s |
| """%(initialTitle, |
| keys.get('FILE','<unknown>'), |
| keys.get('LINE','<unknown>')) |
| |
| reporterSelections = [] |
| reporterOptions = [] |
| |
| try: |
| active = self.server.config.getint('ScanView','reporter') |
| except: |
| active = 0 |
| for i,r in enumerate(self.server.reporters): |
| selected = (i == active) |
| if selected: |
| selectedStr = ' selected' |
| else: |
| selectedStr = '' |
| reporterSelections.append('<option value="%d"%s>%s</option>'%(i,selectedStr,r.getName())) |
| options = '\n'.join(["""\ |
| <tr> |
| <td class="form_clabel">%s:</td> |
| <td class="form_value"><input type="text" name="%s_%s" value="%s"></td> |
| </tr>"""%(o,r.getName(),o,self.server.config.get(r.getName(), o)) for o in r.getParameterNames()]) |
| display = ('none','')[selected] |
| reporterOptions.append("""\ |
| <tr id="%sReporterOptions" style="display:%s"> |
| <td class="form_label">%s Options</td> |
| <td class="form_value"> |
| <table class="form_inner_group"> |
| %s |
| </table> |
| </td> |
| </tr> |
| """%(r.getName(),display,r.getName(),options)) |
| reporterSelections = '\n'.join(reporterSelections) |
| reporterOptionsDivs = '\n'.join(reporterOptions) |
| reportersArray = '[%s]'%(','.join([`r.getName()` for r in self.server.reporters])) |
| |
| result = """<html> |
| <head> |
| <title>File Bug</title> |
| <link rel="stylesheet" type="text/css" href="/scanview.css" /> |
| </head> |
| <script language="javascript" type="text/javascript"> |
| var reporters = %(reportersArray)s; |
| function updateReporterOptions() { |
| index = document.getElementById('reporter').selectedIndex; |
| for (var i=0; i < reporters.length; ++i) { |
| o = document.getElementById(reporters[i] + "ReporterOptions"); |
| if (i == index) { |
| o.style.display = ""; |
| } else { |
| o.style.display = "none"; |
| } |
| } |
| } |
| </script> |
| <body onLoad="updateReporterOptions()"> |
| <h3> |
| <a href="/">Summary</a> > |
| <a href="/report-%(report)s.html">Report %(report)s</a> > |
| File Bug</h3> |
| <form name="form" action="/report_submit" method="post"> |
| <input type="hidden" name="report" value="%(report)s"> |
| |
| <table class="form"> |
| <tr><td> |
| <table class="form_group"> |
| <tr> |
| <td class="form_clabel">Title:</td> |
| <td class="form_value"> |
| <input type="text" name="title" size="50" value="%(initialTitle)s"> |
| </td> |
| </tr> |
| <tr> |
| <td class="form_label">Description:</td> |
| <td class="form_value"> |
| <textarea rows="10" cols="80" name="description"> |
| %(initialDescription)s |
| </textarea> |
| </td> |
| </table> |
| <br> |
| <table class="form_group"> |
| <tr> |
| <td class="form_clabel">Method:</td> |
| <td class="form_value"> |
| <select id="reporter" name="reporter" onChange="updateReporterOptions()"> |
| %(reporterSelections)s |
| </select> |
| </td> |
| </tr> |
| %(reporterOptionsDivs)s |
| </table> |
| <br> |
| </td></tr> |
| <tr><td class="form_submit"> |
| <input align="right" type="submit" name="Submit" value="Submit"> |
| </td></tr> |
| </table> |
| </form> |
| |
| <iframe src="/report-%(report)s.html" width="100%%" height="40%%" |
| scrolling="auto" frameborder="1"> |
| <a href="/report-%(report)s.html">View Bug Report</a> |
| </iframe> |
| |
| </body> |
| </html>"""%locals() |
| return self.send_string(result) |
| |
| def send_head(self, fields=None): |
| if fields is None: |
| fields = {} |
| self.fields = fields |
| |
| o = urlparse.urlparse(self.path) |
| self.fields = parse_query(o.query, fields) |
| path = posixpath.normpath(urllib.unquote(o.path)) |
| |
| # Split the components and strip the root prefix. |
| components = path.split('/')[1:] |
| |
| # Special case some top-level entries. |
| if components: |
| name = components[0] |
| if len(components)==2: |
| if name=='report': |
| return self.send_report(components[1]) |
| elif name=='open': |
| return self.send_open_report(components[1]) |
| elif len(components)==1: |
| if name=='quit': |
| self.server.halt() |
| return self.send_string('Goodbye.', 'text/plain') |
| elif name=='report_submit': |
| return self.send_report_submit() |
| elif name=='favicon.ico': |
| return self.send_path(posixpath.join(kResources,'bugcatcher.ico')) |
| |
| # Match directory entries. |
| if components[-1] == '': |
| components[-1] = 'index.html' |
| |
| suffix = '/'.join(components) |
| |
| # The summary may reference source files on disk using rooted |
| # paths. Make sure these resolve correctly for now. |
| # FIXME: This isn't a very good idea... we should probably |
| # mark rooted paths somehow. |
| if os.path.exists(posixpath.join('/', suffix)): |
| path = posixpath.join('/', suffix) |
| else: |
| path = posixpath.join(self.server.root, suffix) |
| |
| if self.server.options.debug > 1: |
| print >>sys.stderr, '%s: SERVER: sending path "%s"'%(sys.argv[0], |
| path) |
| return self.send_path(path) |
| |
| def send_404(self): |
| self.send_error(404, "File not found") |
| return None |
| |
| def send_path(self, path): |
| ctype = self.guess_type(path) |
| if ctype.startswith('text/'): |
| # Patch file instead |
| return self.send_patched_file(path, ctype) |
| else: |
| mode = 'rb' |
| try: |
| f = open(path, mode) |
| except IOError: |
| return self.send_404() |
| return self.send_file(f, ctype) |
| |
| def send_file(self, f, ctype): |
| # Patch files to add links, but skip binary files. |
| self.send_response(200) |
| self.send_header("Content-type", ctype) |
| fs = os.fstat(f.fileno()) |
| self.send_header("Content-Length", str(fs[6])) |
| self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) |
| self.end_headers() |
| return f |
| |
| def send_string(self, s, ctype='text/html', headers=True, mtime=None): |
| if headers: |
| self.send_response(200) |
| self.send_header("Content-type", ctype) |
| self.send_header("Content-Length", str(len(s))) |
| if mtime is None: |
| mtime = self.dynamic_mtime |
| self.send_header("Last-Modified", self.date_time_string(mtime)) |
| self.end_headers() |
| return StringIO.StringIO(s) |
| |
| def send_patched_file(self, path, ctype): |
| # Allow a very limited set of variables. This is pretty gross. |
| variables = {} |
| variables['report'] = '' |
| m = kReportFileRE.match(path) |
| if m: |
| variables['report'] = m.group(2) |
| |
| try: |
| f = open(path,'r') |
| except IOError: |
| return self.send_404() |
| fs = os.fstat(f.fileno()) |
| data = f.read() |
| for a,b in kReportReplacements: |
| data = a.sub(b % variables, data) |
| return self.send_string(data, ctype, mtime=fs.st_mtime) |
| |
| |
| def create_server(address, options, root): |
| import Reporter |
| |
| reporters = Reporter.getReporters() |
| |
| return ScanViewServer(address, ScanViewRequestHandler, |
| root, |
| reporters, |
| options) |