From: Daniel Dunbar Date: Fri, 19 Sep 2008 23:32:11 +0000 (+0000) Subject: Add initial implementation of scan-view X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=e33d3682b6f4ba798b9d8d6f395ac8003827c03b;p=clang Add initial implementation of scan-view - Web based interface to static analyzer. git-svn-id: https://llvm.org/svn/llvm-project/cfe/trunk@56375 91177308-0d34-0410-b5e6-96231b3b80d8 --- diff --git a/tools/scan-view/Reporter.py b/tools/scan-view/Reporter.py new file mode 100644 index 0000000000..a28d0945ac --- /dev/null +++ b/tools/scan-view/Reporter.py @@ -0,0 +1,159 @@ +"""Methods for reporting bugs.""" + +import subprocess, sys, os + +__all__ = ['BugReport', 'getReporters'] + +# Collect information about a bug. + +class BugReport: + def __init__(self, title, description, files): + self.title = title + self.description = description + self.files = files + +# Reporter interfaces. + +import os + +import email, mimetypes, smtplib +from email import encoders +from email.message import Message +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +class EmailReporter: + def getName(self): + return 'Email' + + def getParameterNames(self): + return ['To', 'From', 'SMTP Server', 'SMTP Port'] + + # Lifted from python email module examples. + def attachFile(self, outer, path): + # Guess the content type based on the file's extension. Encoding + # will be ignored, although we should check for simple things like + # gzip'd or compressed files. + ctype, encoding = mimetypes.guess_type(path) + if ctype is None or encoding is not None: + # No guess could be made, or the file is encoded (compressed), so + # use a generic bag-of-bits type. + ctype = 'application/octet-stream' + maintype, subtype = ctype.split('/', 1) + if maintype == 'text': + fp = open(path) + # Note: we should handle calculating the charset + msg = MIMEText(fp.read(), _subtype=subtype) + fp.close() + else: + fp = open(path, 'rb') + msg = MIMEBase(maintype, subtype) + msg.set_payload(fp.read()) + fp.close() + # Encode the payload using Base64 + encoders.encode_base64(msg) + # Set the filename parameter + msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(path)) + outer.attach(msg) + + def fileReport(self, report, parameters): + mainMsg = """\ +BUG REPORT +--- +Title: %s +Description: %s +"""%(report.title, report.description) + + if not parameters.get('To') or not parameters.get('From'): + raise ValueError,'Invalid email parameters.' + + msg = MIMEMultipart() + msg['Subject'] = 'BUG REPORT: %s'%(report.title) + # FIXME: Get config parameters + msg['To'] = parameters.get('To') + msg['From'] = parameters.get('From') + msg.preamble = mainMsg + + msg.attach(MIMEText(mainMsg, _subtype='text/plain')) + for file in report.files: + self.attachFile(msg, file) + + s = smtplib.SMTP(host=parameters.get('SMTP Server'), + port=parameters.get('SMTP Port')) + s.sendmail(msg['From'], msg['To'], msg.as_string()) + s.close() + +class BugzillaReporter: + def getName(self): + return 'Bugzilla' + + def getParameterNames(self): + return ['URL', 'Product'] + + def fileReport(self, report, parameters): + raise NotImplementedError + +class RadarReporter: + @staticmethod + def isAvailable(): + # FIXME: Find this .scpt better + path = os.path.join(os.path.dirname(__file__),'Resources/GetRadarVersion.scpt') + try: + p = subprocess.Popen(['osascript',path], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + return False + data,err = p.communicate() + res = p.wait() + # FIXME: Check version? Check for no errors? + return res == 0 + + def getName(self): + return 'Radar' + + def getParameterNames(self): + return ['Component', 'Component Version'] + + def fileReport(self, report, parameters): + component = parameters.get('Component', '') + componentVersion = parameters.get('Component Version', '') + personID = "" + diagnosis = "" + config = "" + + if not component.strip(): + component = 'Bugs found by clang Analyzer' + if not componentVersion.strip(): + componentVersion = 'X' + + script = os.path.join(os.path.dirname(__file__),'Resources/FileRadar.scpt') + args = ['osascript', script, component, componentVersion, personID, report.title, + report.description, diagnosis, config] + map(os.path.abspath, report.files) +# print >>sys.stderr, args + try: + p = subprocess.Popen(args, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + print >>sys.stderr, '%s: SERVER: radar failed'%(sys.argv[0],) + sys.print_exc() + raise + data, err = p.communicate() +# print >>sys.stderr, '%s: SERVER: radar report: "%s" "%s"'%(sys.argv[0],data, err) + res = p.wait() +# print >>sys.stderr, '%s: SERVER: radar report res: %d'%(sys.argv[0],res,) + + if res: + raise RuntimeError,'Radar submission failed.' + + return data.replace('\n','\n
') + +### + +def getReporters(): + reporters = [] + if RadarReporter.isAvailable(): + reporters.append(RadarReporter()) + reporters.extend([EmailReporter(), BugzillaReporter()]) + return reporters + diff --git a/tools/scan-view/Resources/FileRadar.scpt b/tools/scan-view/Resources/FileRadar.scpt new file mode 100644 index 0000000000..145386c30c Binary files /dev/null and b/tools/scan-view/Resources/FileRadar.scpt differ diff --git a/tools/scan-view/Resources/GetRadarVersion.scpt b/tools/scan-view/Resources/GetRadarVersion.scpt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/scan-view/ScanView.py b/tools/scan-view/ScanView.py new file mode 100644 index 0000000000..e340a9fa6c --- /dev/null +++ b/tools/scan-view/ScanView.py @@ -0,0 +1,388 @@ +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 + +from Reporter import BugReport + +# Keys replaced by server. + +kReportBugRE = re.compile('') +kReportBugRepl = 'Report Bug' +kBugKeyValueRE = re.compile('') + +### + +__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.status = None + + def run(self): + result = None + try: + if self.server.options.debug: + print >>sys.stderr, "%s: SERVER: submitting bug."%(sys.argv[0],) + result = self.reporter.fileReport(self.report, self.parameters) + time.sleep(3) + if self.server.options.debug: + print >>sys.stderr, "%s: SERVER: submission complete."%(sys.argv[0],) + except Exception,e: + s = StringIO.StringIO() + import traceback + print >>s,'Submission Failed
'
+            traceback.print_exc(e,file=s)
+            print >>s,'
' + self.status = s.getvalue() + return + + s = StringIO.StringIO() + print >>s, 'Submission Complete!' + print >>s, '
' + if result is not None: + print >>s, result + 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 + + 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 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(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__ + + 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 send_internal_error(self, message): + return self.send_string('ERROR: %s'%(message,), 'text/plain') + + def send_report_submit(self): + s = StringIO.StringIO() + report = self.fields.get('report') + reporter = self.fields.get('reporter') + title = self.fields.get('title') + description = self.fields.get('description') + + # Get the reporter and parameters. + reporter = self.server.reporters[int(reporter)] + parameters = {} + for o in reporter.getParameterNames(): + name = '%s_%s'%(reporter.getName(),o) + parameters[o] = self.fields.get(name) + + # Create the report. + path = os.path.join(self.server.root, 'report-%s.html'%report) + files = [path] + br = BugReport(title, description, files) + + # Send back an initial response and wait for the report to + # finish. + initial_response = """ +Filing Report + +

Filing Report

+Report: %(report)s
+Title: %(title)s
+Description: %(description)s
+
+Submission in progress."""%locals() + + self.send_response(200) + self.send_header("Content-type", 'text/html') + self.end_headers() + self.wfile.write(initial_response) + self.wfile.flush() + + # Kick off a reporting thread. + t = ReporterThread(br, reporter, parameters, self.server) + t.start() + + # Wait for thread to die... + while t.isAlive(): + self.wfile.write('.') + self.wfile.flush() + time.sleep(.25) + submitStatus = t.status + + end_response = """
+%(submitStatus)s +
+Home + + +"""%locals() + return self.send_string(end_response, headers=False) + + def send_report(self, report): + try: + keys = self.load_report(report) + except IOError: + return self.send_internal_error('Invalid report.') + + initialTitle = keys.get('DESC','') + initialDescription = 'Bug generated by the clang static analyzer.' + + keysAndValues = '\n'.join(['%s: %s
'%(k,v) for k,v in keys.items()]) + reporterSelections = [] + reporterOptions = [] + + for i,r in enumerate(self.server.reporters): + reporterSelections.append(''%(i,r.getName())) + options = '\n'.join(['%s:
'%(o,r.getName(),o) for o in r.getParameterNames()]) + if i==0: + display = 'inline' + else: + display = 'none' + reporterOptions.append('
\n

%s Options

%s\n
'%(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 = """ + + File Report + + + +

File Report

+%(keysAndValues)s +
+
+Title: +
+Description:
+
+
+ +Method:
+
+%(reporterOptionsDivs)s +
+ +
+ +"""%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 name=='quit': + self.server.halt() + return self.send_string('Goodbye.', 'text/plain') + elif name=='report': + if len(components)==2: + return self.send_report(components[1]) + else: + return self.send_404() + elif name=='report_submit': + if len(components)==1: + return self.send_report_submit() + else: + return self.send_404() + + # Match directory entries. + if components[-1] == '': + components[-1] = 'index.html' + + path = posixpath.join(self.server.root, '/'.join(components)) + 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: + self.send_header("Last-Modified", self.date_time_string(mtime)) + self.end_headers() + return StringIO.StringIO(s) + + def send_patched_file(self, path, ctype): + f = open(path,'r') + fs = os.fstat(f.fileno()) + data = f.read() + data = kReportBugRE.sub(kReportBugRepl, data) + return self.send_string(data, ctype, mtime=fs.st_mtime) + + +def create_server(options, root): + import Reporter + + reporters = Reporter.getReporters() + + return ScanViewServer((options.host, options.port), + ScanViewRequestHandler, + root, + reporters, + options) diff --git a/tools/scan-view/scan-view b/tools/scan-view/scan-view new file mode 100755 index 0000000000..db3d3bb87c --- /dev/null +++ b/tools/scan-view/scan-view @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +"""The clang static analyzer results viewer. +""" + +import sys +import thread +import time +import urllib +import webbrowser + +# How long to wait for server to start. +kSleepTimeout = .05 +kMaxSleeps = 100 + +# Default server parameters + +kDefaultHost = 'localhost' +kDefaultPort = 8181 + +### + +def url_is_up(url): + try: + o = urllib.urlopen(url) + except IOError: + return False + o.close() + return True + +def start_browser(options): + import urllib, webbrowser + + url = 'http://%s:%d'%(options.host, options.port) + + # Wait for server to start... + if options.debug: + sys.stderr.write('%s: Waiting for server.' % sys.argv[0]) + sys.stderr.flush() + for i in range(kMaxSleeps): + if url_is_up(url): + break + if options.debug: + sys.stderr.write('.') + sys.stderr.flush() + time.sleep(kSleepTimeout) + else: + print >>sys.stderr,'WARNING: Unable to detect that server started.' + + if options.debug: + print >>sys.stderr,'%s: Starting webbrowser...' % sys.argv[0] + webbrowser.open(url) + +def run(options, root): + import ScanView + try: + if options.debug: + print >>sys.stderr,'%s: SERVER: starting %s:%d'%(sys.argv[0], + options.host, + options.port) + httpd = ScanView.create_server(options, root) + httpd.serve_forever() + except KeyboardInterrupt: + pass + +def main(): + from optparse import OptionParser + parser = OptionParser('usage: %prog [options] ') + parser.set_description(__doc__) + parser.add_option( + '--host', dest="host", default=kDefaultHost, type="string", + help="Host interface to listen on. (default=%s)" % kDefaultHost) + parser.add_option( + '--port', dest="port", default=kDefaultPort, type="int", + help="Port to listen on. (default=%s)" % kDefaultPort) + parser.add_option("--debug", dest="debug", default=0, + action="count", + help="Print additional debugging information.") + parser.add_option("--no-browser", dest="startBrowser", default=True, + action="store_false", + help="Don't open a webbrowser on startup.") + (options, args) = parser.parse_args() + + if len(args) != 1: + parser.error('invalid number of arguments.') + root, = args + + # Kick off thread to wait for server and start web browser, if + # requested. + if options.startBrowser: + t = thread.start_new_thread(start_browser, (options,)) + + run(options, root) + +if __name__ == '__main__': + main()