2 import SimpleHTTPServer
5 import urllib, urlparse
19 # Various patterns matched or replaced by server.
21 kReportFileRE = re.compile('(.*/)?report-(.*)\\.html')
23 kBugKeyValueRE = re.compile('<!-- BUG([^ ]*) (.*) -->')
25 # <!-- REPORTPROBLEM file="crashes/clang_crash_ndSGF9.mi" stderr="crashes/clang_crash_ndSGF9.mi.stderr.txt" info="crashes/clang_crash_ndSGF9.mi.info" -->
27 kReportCrashEntryRE = re.compile('<!-- REPORTPROBLEM (.*?)-->')
28 kReportCrashEntryKeyValueRE = re.compile(' ?([^=]+)="(.*?)"')
30 kReportReplacements = []
32 # Add custom javascript.
33 kReportReplacements.append((re.compile('<!-- SUMMARYENDHEAD -->'), """\
34 <script language="javascript" type="text/javascript">
36 if (window.XMLHttpRequest) {
37 req = new XMLHttpRequest();
38 } else if (window.ActiveXObject) {
39 req = new ActiveXObject("Microsoft.XMLHTTP");
41 if (req != undefined) {
42 req.open("GET", url, true);
48 # Insert additional columns.
49 kReportReplacements.append((re.compile('<!-- REPORTBUGCOL -->'),
50 '<td></td><td></td>'))
52 # Insert report bug and open file links.
53 kReportReplacements.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>')))
57 kReportReplacements.append((re.compile('<!-- REPORTHEADER -->'),
58 '<h3><a href="/">Summary</a> > Report %(report)s</h3>'))
60 kReportReplacements.append((re.compile('<!-- REPORTSUMMARYEXTRA -->'),
61 '<td class="Button"><a href="report/%(report)s">Report Bug</a></td>'))
63 # Insert report crashes link.
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
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>.'))
74 # Other simple parameters
76 kResources = posixpath.join(posixpath.dirname(__file__), 'Resources')
77 kConfigPath = os.path.expanduser('~/.scanview.cfg')
83 __all__ = ["create_server"]
85 class ReporterThread(threading.Thread):
86 def __init__(self, report, reporter, parameters, server):
87 threading.Thread.__init__(self)
90 self.reporter = reporter
91 self.parameters = parameters
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)
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
108 s = StringIO.StringIO()
110 print >>s,'<b>Unhandled Exception</b><br><pre>'
111 traceback.print_exc(e,file=s)
113 self.status = s.getvalue()
115 class ScanViewServer(BaseHTTPServer.HTTPServer):
116 def __init__(self, address, handler, root, reporters, options):
117 BaseHTTPServer.HTTPServer.__init__(self, address, handler)
119 self.reporters = reporters
120 self.options = options
125 def load_config(self):
126 self.config = ConfigParser.RawConfigParser()
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(), '')
136 # Ignore parse errors
138 self.config.read([kConfigPath])
144 atexit.register(lambda: self.save_config())
146 def save_config(self):
147 # Ignore errors (only called on exit).
149 f = open(kConfigPath,'w')
157 if self.options.debug:
158 print >>sys.stderr, "%s: SERVER: halting." % (sys.argv[0],)
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],)
165 self.handle_request()
167 print 'OSError',e.errno
169 def finish_request(self, request, client_address):
170 if self.options.autoReload:
172 self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler
173 BaseHTTPServer.HTTPServer.finish_request(self, request, client_address)
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],)
182 BaseHTTPServer.HTTPServer.handle_error(self, request, client_address)
184 # Borrowed from Quixote, with simplifications.
185 def parse_query(qs, fields=None):
188 for chunk in filter(None, qs.split('&')):
193 name, value = chunk.split('=', 1)
194 name = urllib.unquote(name.replace('+', ' '))
195 value = urllib.unquote(value.replace('+', ' '))
196 item = fields.get(name)
198 fields[name] = [value]
203 class ScanViewRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
204 server_version = "ScanViewServer/" + __version__
205 dynamic_mtime = time.time()
209 SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self)
211 self.handle_exception(e)
215 SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
217 self.handle_exception(e)
220 """Serve a POST request."""
222 length = self.headers.getheader('content-length') or "0"
227 content = self.rfile.read(length)
228 fields = parse_query(content)
229 f = self.send_head(fields)
231 self.copyfile(f, self.wfile)
234 self.handle_exception(e)
236 def log_message(self, format, *args):
237 if self.server.options.debug:
238 sys.stderr.write("%s: SERVER: %s - - [%s] %s\n" %
240 self.address_string(),
241 self.log_date_time_string(),
244 def load_report(self, report):
245 path = os.path.join(self.server.root, 'report-%s.html'%report)
246 data = open(path).read()
248 for item in kBugKeyValueRE.finditer(data):
253 def load_crashes(self):
254 path = posixpath.join(self.server.root, 'index.html')
255 data = open(path).read()
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)
264 def handle_exception(self, exc):
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')
271 self.copyfile(f, self.wfile)
274 def get_scalar_field(self, name):
275 if name in self.fields:
276 return self.fields[name][0]
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')
286 for fileID in self.fields.get('files',[]):
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])
296 return (False, "Missing title.")
298 return (False, "Missing description.")
300 reporterIndex = int(reporterIndex)
302 return (False, "Invalid report method.")
304 # Get the reporter and parameters.
305 reporter = self.server.reporters[reporterIndex]
307 for o in reporter.getParameters():
308 name = '%s_%s'%(reporter.getName(),o.getName())
309 if name not in self.fields:
311 'Missing field "%s" for %s report method.'%(name,
313 parameters[o.getName()] = self.get_scalar_field(name)
315 # Update config defaults.
317 self.server.config.set('ScanView', 'reporter', reporterIndex)
318 for o in reporter.getParameters():
319 if o.saveConfigValue():
321 self.server.config.set(reporter.getName(), name, parameters[name])
324 bug = Reporter.BugReport(title, description, files)
326 # Kick off a reporting thread.
327 t = ReporterThread(bug, reporter, parameters, self.server)
330 # Wait for thread to die...
333 submitStatus = t.status
335 return (t.success, t.status)
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 > "
343 <a href="/report_crashes">File Bug</a> > """%locals()
345 reportingFor = '<a href="/%s">Report %s</a> > ' % (c.reportSource,
347 fileBug = '<a href="/report/%s">File Bug</a> > ' % report
348 title = self.get_scalar_field('title')
349 description = self.get_scalar_field('description')
351 res,message = self.submit_bug(c)
354 statusClass = 'SubmitOk'
355 statusName = 'Succeeded'
357 statusClass = 'SubmitFail'
358 statusName = 'Failed'
362 <title>Bug Submission</title>
363 <link rel="stylesheet" type="text/css" href="/scanview.css" />
367 <a href="/">Summary</a> >
371 <form name="form" action="">
374 <table class="form_group">
376 <td class="form_clabel">Title:</td>
377 <td class="form_value">
378 <input type="text" name="title" size="50" value="%(title)s" disabled>
382 <td class="form_label">Description:</td>
383 <td class="form_value">
384 <textarea rows="10" cols="80" name="description" disabled>
392 <h1 class="%(statusClass)s">Submission %(statusName)s</h1>
396 <a href="/">Return to Summary</a>
399 return self.send_string(result)
401 def send_open_report(self, report):
403 keys = self.load_report(report)
405 return self.send_error(400, 'Invalid report.')
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)
412 if self.server.options.debug:
413 print >>sys.stderr, '%s: SERVER: opening "%s"'%(sys.argv[0],
416 status = startfile.open(file)
418 res = 'Opened: "%s"' % file
420 res = 'Open failed: "%s"' % file
422 return self.send_string(res, 'text/plain')
424 def get_report_context(self, report):
427 if report is None or report == 'None':
428 data = self.load_crashes()
429 # Don't allow empty reports.
431 raise ValueError, 'No crashes detected!'
433 c.title = 'clang static analyzer failures'
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',
446 The clang static analyzer failed on these inputs:
452 """ % ('\n'.join([item.get('src','<unknown>') for item in data]),
454 c.reportSource = None
455 c.navMarkup = "Report Crashes > "
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
469 c.files = [f for f in c.files
470 if os.path.exists(f) and os.path.isfile(f)]
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)
478 c.title = keys.get('DESC','clang error (unrecognized')
480 Bug reported by the clang static analyzer.
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,
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)
501 # report is None is used for crashes
503 c = self.get_report_context(report)
504 except ValueError, e:
505 return self.send_error(400, e.message)
508 description= c.description
509 reportingFor = c.navMarkup
510 if c.reportSource is None:
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)
519 reporterSelections = []
523 active = int(getConfigOption('ScanView','reporter'))
526 for i,r in enumerate(self.server.reporters):
527 selected = (i == active)
529 selectedStr = ' selected'
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">
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]))
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)])
555 <td class="form_label">Attach:</td>
556 <td class="form_value">
557 <select style="width:100%%" name="files" multiple size=%d>
562 """ % (min(5, len(c.files)), attachFileOptions)
568 <title>File Bug</title>
569 <link rel="stylesheet" type="text/css" href="/scanview.css" />
571 <script language="javascript" type="text/javascript">
572 var reporters = %(reportersArray)s;
573 function updateReporterOptions() {
574 index = document.getElementById('reporter').selectedIndex;
575 for (var i=0; i < reporters.length; ++i) {
576 o = document.getElementById(reporters[i] + "ReporterOptions");
578 o.style.display = "";
580 o.style.display = "none";
585 <body onLoad="updateReporterOptions()">
587 <a href="/">Summary</a> >
590 <form name="form" action="/report_submit" method="post">
591 <input type="hidden" name="report" value="%(report)s">
595 <table class="form_group">
597 <td class="form_clabel">Title:</td>
598 <td class="form_value">
599 <input type="text" name="title" size="50" value="%(title)s">
603 <td class="form_label">Description:</td>
604 <td class="form_value">
605 <textarea rows="10" cols="80" name="description">
615 <table class="form_group">
617 <td class="form_clabel">Method:</td>
618 <td class="form_value">
619 <select id="reporter" name="reporter" onChange="updateReporterOptions()">
620 %(reporterSelections)s
624 %(reporterOptionsDivs)s
628 <tr><td class="form_submit">
629 <input align="right" type="submit" name="Submit" value="Submit">
639 return self.send_string(result)
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.')
650 o = urlparse.urlparse(self.path)
651 self.fields = parse_query(o.query, fields)
652 path = posixpath.normpath(urllib.unquote(o.path))
654 # Split the components and strip the root prefix.
655 components = path.split('/')[1:]
657 # Special case some top-level entries.
660 if len(components)==2:
662 return self.send_report(components[1])
664 return self.send_open_report(components[1])
665 elif len(components)==1:
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' : {},
675 for i,r in enumerate(self.server.reporters):
676 if r.getName() == 'Radar':
677 overrides['ScanView']['reporter'] = i
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(kResources,'bugcatcher.ico'))
685 # Match directory entries.
686 if components[-1] == '':
687 components[-1] = 'index.html'
689 relpath = '/'.join(components)
690 path = posixpath.join(self.server.root, relpath)
692 if self.server.options.debug > 1:
693 print >>sys.stderr, '%s: SERVER: sending path "%s"'%(sys.argv[0],
695 return self.send_path(path)
698 self.send_error(404, "File not found")
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()
707 ctype = self.guess_type(path)
708 if ctype.startswith('text/'):
710 return self.send_patched_file(path, ctype)
716 return self.send_404()
717 return self.send_file(f, ctype)
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))
729 def send_string(self, s, ctype='text/html', headers=True, mtime=None):
731 self.send_response(200)
732 self.send_header("Content-type", ctype)
733 self.send_header("Content-Length", str(len(s)))
735 mtime = self.dynamic_mtime
736 self.send_header("Last-Modified", self.date_time_string(mtime))
738 return StringIO.StringIO(s)
740 def send_patched_file(self, path, ctype):
741 # Allow a very limited set of variables. This is pretty gross.
743 variables['report'] = ''
744 m = kReportFileRE.match(path)
746 variables['report'] = m.group(2)
751 return self.send_404()
752 fs = os.fstat(f.fileno())
754 for a,b in kReportReplacements:
755 data = a.sub(b % variables, data)
756 return self.send_string(data, ctype, mtime=fs.st_mtime)
759 def create_server(address, options, root):
762 reporters = Reporter.getReporters()
764 return ScanViewServer(address, ScanViewRequestHandler,