blob: 7dc0351ebe7f56378697973180bf4a158675f711 [file] [log] [blame]
Andrew Scull5e1ddfa2018-08-14 10:06:54 +01001import 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)