1 from __future__
import print_function
4 from http
.server
import HTTPServer
, SimpleHTTPRequestHandler
6 from BaseHTTPServer
import HTTPServer
7 from SimpleHTTPServer
import SimpleHTTPRequestHandler
12 from urlparse
import urlparse
13 from urllib
import unquote
15 from urllib
.parse
import urlparse
, unquote
19 if sys
.version_info
.major
>= 3:
20 from io
import StringIO
, BytesIO
22 from io
import BytesIO
, BytesIO
as StringIO
36 import ConfigParser
as configparser
39 # Various patterns matched or replaced by server.
41 kReportFileRE
= re
.compile("(.*/)?report-(.*)\\.html")
43 kBugKeyValueRE
= re
.compile("<!-- BUG([^ ]*) (.*) -->")
45 # <!-- REPORTPROBLEM file="crashes/clang_crash_ndSGF9.mi" stderr="crashes/clang_crash_ndSGF9.mi.stderr.txt" info="crashes/clang_crash_ndSGF9.mi.info" -->
47 kReportCrashEntryRE
= re
.compile("<!-- REPORTPROBLEM (.*?)-->")
48 kReportCrashEntryKeyValueRE
= re
.compile(' ?([^=]+)="(.*?)"')
50 kReportReplacements
= []
52 # Add custom javascript.
53 kReportReplacements
.append(
55 re
.compile("<!-- SUMMARYENDHEAD -->"),
57 <script language="javascript" type="text/javascript">
59 if (window.XMLHttpRequest) {
60 req = new XMLHttpRequest();
61 } else if (window.ActiveXObject) {
62 req = new ActiveXObject("Microsoft.XMLHTTP");
64 if (req != undefined) {
65 req.open("GET", url, true);
73 # Insert additional columns.
74 kReportReplacements
.append((re
.compile("<!-- REPORTBUGCOL -->"), "<td></td><td></td>"))
76 # Insert report bug and open file links.
77 kReportReplacements
.append(
79 re
.compile('<!-- REPORTBUG id="report-(.*)\\.html" -->'),
81 '<td class="Button"><a href="report/\\1">Report Bug</a></td>'
82 + '<td class="Button"><a href="javascript:load(\'open/\\1\')">Open File</a></td>'
87 kReportReplacements
.append(
89 re
.compile("<!-- REPORTHEADER -->"),
90 '<h3><a href="/">Summary</a> > Report %(report)s</h3>',
94 kReportReplacements
.append(
96 re
.compile("<!-- REPORTSUMMARYEXTRA -->"),
97 '<td class="Button"><a href="report/%(report)s">Report Bug</a></td>',
101 # Insert report crashes link.
103 # Disabled for the time being until we decide exactly when this should
104 # be enabled. Also the radar reporter needs to be fixed to report
107 # kReportReplacements.append((re.compile('<!-- REPORTCRASHES -->'),
108 # '<br>These files will automatically be attached to ' +
109 # 'reports filed here: <a href="report_crashes">Report Crashes</a>.'))
112 # Other simple parameters
114 kShare
= posixpath
.join(posixpath
.dirname(__file__
), "../share/scan-view")
115 kConfigPath
= os
.path
.expanduser("~/.scanview.cfg")
121 __all__
= ["create_server"]
124 class ReporterThread(threading
.Thread
):
125 def __init__(self
, report
, reporter
, parameters
, server
):
126 threading
.Thread
.__init
__(self
)
129 self
.reporter
= reporter
130 self
.parameters
= parameters
137 if self
.server
.options
.debug
:
138 print("%s: SERVER: submitting bug." % (sys
.argv
[0],), file=sys
.stderr
)
139 self
.status
= self
.reporter
.fileReport(self
.report
, self
.parameters
)
142 if self
.server
.options
.debug
:
144 "%s: SERVER: submission complete." % (sys
.argv
[0],), file=sys
.stderr
146 except Reporter
.ReportFailure
as e
:
147 self
.status
= e
.value
148 except Exception as e
:
152 print("<b>Unhandled Exception</b><br><pre>", file=s
)
153 traceback
.print_exc(file=s
)
154 print("</pre>", file=s
)
155 self
.status
= s
.getvalue()
158 class ScanViewServer(HTTPServer
):
159 def __init__(self
, address
, handler
, root
, reporters
, options
):
160 HTTPServer
.__init
__(self
, address
, handler
)
162 self
.reporters
= reporters
163 self
.options
= options
168 def load_config(self
):
169 self
.config
= configparser
.RawConfigParser()
172 self
.config
.add_section("ScanView")
173 for r
in self
.reporters
:
174 self
.config
.add_section(r
.getName())
175 for p
in r
.getParameters():
176 if p
.saveConfigValue():
177 self
.config
.set(r
.getName(), p
.getName(), "")
179 # Ignore parse errors
181 self
.config
.read([kConfigPath
])
188 atexit
.register(lambda: self
.save_config())
190 def save_config(self
):
191 # Ignore errors (only called on exit).
193 f
= open(kConfigPath
, "w")
201 if self
.options
.debug
:
202 print("%s: SERVER: halting." % (sys
.argv
[0],), file=sys
.stderr
)
204 def serve_forever(self
):
205 while not self
.halted
:
206 if self
.options
.debug
> 1:
207 print("%s: SERVER: waiting..." % (sys
.argv
[0],), file=sys
.stderr
)
209 self
.handle_request()
211 print("OSError", e
.errno
)
213 def finish_request(self
, request
, client_address
):
214 if self
.options
.autoReload
:
217 self
.RequestHandlerClass
= reload(ScanView
).ScanViewRequestHandler
218 HTTPServer
.finish_request(self
, request
, client_address
)
220 def handle_error(self
, request
, client_address
):
221 # Ignore socket errors
222 info
= sys
.exc_info()
223 if info
and isinstance(info
[1], socket
.error
):
224 if self
.options
.debug
> 1:
226 "%s: SERVER: ignored socket error." % (sys
.argv
[0],),
230 HTTPServer
.handle_error(self
, request
, client_address
)
233 # Borrowed from Quixote, with simplifications.
234 def parse_query(qs
, fields
=None):
237 for chunk
in (_f
for _f
in qs
.split("&") if _f
):
242 name
, value
= chunk
.split("=", 1)
243 name
= unquote(name
.replace("+", " "))
244 value
= unquote(value
.replace("+", " "))
245 item
= fields
.get(name
)
247 fields
[name
] = [value
]
253 class ScanViewRequestHandler(SimpleHTTPRequestHandler
):
254 server_version
= "ScanViewServer/" + __version__
255 dynamic_mtime
= time
.time()
259 SimpleHTTPRequestHandler
.do_HEAD(self
)
260 except Exception as e
:
261 self
.handle_exception(e
)
265 SimpleHTTPRequestHandler
.do_GET(self
)
266 except Exception as e
:
267 self
.handle_exception(e
)
270 """Serve a POST request."""
272 length
= self
.headers
.getheader("content-length") or "0"
277 content
= self
.rfile
.read(length
)
278 fields
= parse_query(content
)
279 f
= self
.send_head(fields
)
281 self
.copyfile(f
, self
.wfile
)
283 except Exception as e
:
284 self
.handle_exception(e
)
286 def log_message(self
, format
, *args
):
287 if self
.server
.options
.debug
:
289 "%s: SERVER: %s - - [%s] %s\n"
292 self
.address_string(),
293 self
.log_date_time_string(),
298 def load_report(self
, report
):
299 path
= os
.path
.join(self
.server
.root
, "report-%s.html" % report
)
300 data
= open(path
).read()
302 for item
in kBugKeyValueRE
.finditer(data
):
307 def load_crashes(self
):
308 path
= posixpath
.join(self
.server
.root
, "index.html")
309 data
= open(path
).read()
311 for item
in kReportCrashEntryRE
.finditer(data
):
312 fieldData
= item
.group(1)
314 [i
.groups() for i
in kReportCrashEntryKeyValueRE
.finditer(fieldData
)]
316 problems
.append(fields
)
319 def handle_exception(self
, exc
):
323 print("INTERNAL ERROR\n", file=s
)
324 traceback
.print_exc(file=s
)
325 f
= self
.send_string(s
.getvalue(), "text/plain")
327 self
.copyfile(f
, self
.wfile
)
330 def get_scalar_field(self
, name
):
331 if name
in self
.fields
:
332 return self
.fields
[name
][0]
336 def submit_bug(self
, c
):
337 title
= self
.get_scalar_field("title")
338 description
= self
.get_scalar_field("description")
339 report
= self
.get_scalar_field("report")
340 reporterIndex
= self
.get_scalar_field("reporter")
342 for fileID
in self
.fields
.get("files", []):
347 if i
is None or i
< 0 or i
>= len(c
.files
):
348 return (False, "Invalid file ID")
349 files
.append(c
.files
[i
])
352 return (False, "Missing title.")
354 return (False, "Missing description.")
356 reporterIndex
= int(reporterIndex
)
358 return (False, "Invalid report method.")
360 # Get the reporter and parameters.
361 reporter
= self
.server
.reporters
[reporterIndex
]
363 for o
in reporter
.getParameters():
364 name
= "%s_%s" % (reporter
.getName(), o
.getName())
365 if name
not in self
.fields
:
368 'Missing field "%s" for %s report method.'
369 % (name
, reporter
.getName()),
371 parameters
[o
.getName()] = self
.get_scalar_field(name
)
373 # Update config defaults.
375 self
.server
.config
.set("ScanView", "reporter", reporterIndex
)
376 for o
in reporter
.getParameters():
377 if o
.saveConfigValue():
379 self
.server
.config
.set(reporter
.getName(), name
, parameters
[name
])
382 bug
= Reporter
.BugReport(title
, description
, files
)
384 # Kick off a reporting thread.
385 t
= ReporterThread(bug
, reporter
, parameters
, self
.server
)
388 # Wait for thread to die...
391 submitStatus
= t
.status
393 return (t
.success
, t
.status
)
395 def send_report_submit(self
):
396 report
= self
.get_scalar_field("report")
397 c
= self
.get_report_context(report
)
398 if c
.reportSource
is None:
399 reportingFor
= "Report Crashes > "
402 <a href="/report_crashes">File Bug</a> > """
406 reportingFor
= '<a href="/%s">Report %s</a> > ' % (c
.reportSource
, report
)
407 fileBug
= '<a href="/report/%s">File Bug</a> > ' % report
408 title
= self
.get_scalar_field("title")
409 description
= self
.get_scalar_field("description")
411 res
, message
= self
.submit_bug(c
)
414 statusClass
= "SubmitOk"
415 statusName
= "Succeeded"
417 statusClass
= "SubmitFail"
418 statusName
= "Failed"
423 <title>Bug Submission</title>
424 <link rel="stylesheet" type="text/css" href="/scanview.css" />
428 <a href="/">Summary</a> >
432 <form name="form" action="">
435 <table class="form_group">
437 <td class="form_clabel">Title:</td>
438 <td class="form_value">
439 <input type="text" name="title" size="50" value="%(title)s" disabled>
443 <td class="form_label">Description:</td>
444 <td class="form_value">
445 <textarea rows="10" cols="80" name="description" disabled>
453 <h1 class="%(statusClass)s">Submission %(statusName)s</h1>
457 <a href="/">Return to Summary</a>
462 return self
.send_string(result
)
464 def send_open_report(self
, report
):
466 keys
= self
.load_report(report
)
468 return self
.send_error(400, "Invalid report.")
470 file = keys
.get("FILE")
471 if not file or not posixpath
.exists(file):
472 return self
.send_error(400, 'File does not exist: "%s"' % file)
476 if self
.server
.options
.debug
:
477 print('%s: SERVER: opening "%s"' % (sys
.argv
[0], file), file=sys
.stderr
)
479 status
= startfile
.open(file)
481 res
= 'Opened: "%s"' % file
483 res
= 'Open failed: "%s"' % file
485 return self
.send_string(res
, "text/plain")
487 def get_report_context(self
, report
):
488 class Context(object):
491 if report
is None or report
== "None":
492 data
= self
.load_crashes()
493 # Don't allow empty reports.
495 raise ValueError("No crashes detected!")
497 c
.title
= "clang static analyzer failures"
502 path
= posixpath
.join(self
.server
.root
, item
["stderr"])
503 if os
.path
.exists(path
):
504 lns
= itertools
.islice(open(path
), 0, 10)
505 stderrSummary
+= "%s\n--\n%s" % (
506 item
.get("src", "<unknown>"),
511 The clang static analyzer failed on these inputs:
518 "\n".join([item
.get("src", "<unknown>") for item
in data
]),
521 c
.reportSource
= None
522 c
.navMarkup
= "Report Crashes > "
525 c
.files
.append(item
.get("src", ""))
526 c
.files
.append(posixpath
.join(self
.server
.root
, item
.get("file", "")))
528 posixpath
.join(self
.server
.root
, item
.get("clangfile", ""))
530 c
.files
.append(posixpath
.join(self
.server
.root
, item
.get("stderr", "")))
531 c
.files
.append(posixpath
.join(self
.server
.root
, item
.get("info", "")))
532 # Just in case something failed, ignore files which don't
534 c
.files
= [f
for f
in c
.files
if os
.path
.exists(f
) and os
.path
.isfile(f
)]
536 # Check that this is a valid report.
537 path
= posixpath
.join(self
.server
.root
, "report-%s.html" % report
)
538 if not posixpath
.exists(path
):
539 raise ValueError("Invalid report ID")
540 keys
= self
.load_report(report
)
542 c
.title
= keys
.get("DESC", "clang error (unrecognized")
544 Bug reported by the clang static analyzer.
551 keys
.get("FILE", "<unknown>"),
552 keys
.get("LINE", "<unknown>"),
554 c
.reportSource
= "report-%s.html" % report
555 c
.navMarkup
= """<a href="/%s">Report %s</a> > """ % (
563 def send_report(self
, report
, configOverrides
=None):
564 def getConfigOption(section
, field
):
566 configOverrides
is not None
567 and section
in configOverrides
568 and field
in configOverrides
[section
]
570 return configOverrides
[section
][field
]
571 return self
.server
.config
.get(section
, field
)
573 # report is None is used for crashes
575 c
= self
.get_report_context(report
)
576 except ValueError as e
:
577 return self
.send_error(400, e
.message
)
580 description
= c
.description
581 reportingFor
= c
.navMarkup
582 if c
.reportSource
is None:
586 <iframe src="/%s" width="100%%" height="40%%"
587 scrolling="auto" frameborder="1">
588 <a href="/%s">View Bug Report</a>
594 reporterSelections
= []
598 active
= int(getConfigOption("ScanView", "reporter"))
601 for i
, r
in enumerate(self
.server
.reporters
):
602 selected
= i
== active
604 selectedStr
= " selected"
607 reporterSelections
.append(
608 '<option value="%d"%s>%s</option>' % (i
, selectedStr
, r
.getName())
611 [o
.getHTML(r
, title
, getConfigOption
) for o
in r
.getParameters()]
613 display
= ("none", "")[selected
]
614 reporterOptions
.append(
616 <tr id="%sReporterOptions" style="display:%s">
617 <td class="form_label">%s Options</td>
618 <td class="form_value">
619 <table class="form_inner_group">
625 % (r
.getName(), display
, r
.getName(), options
)
627 reporterSelections
= "\n".join(reporterSelections
)
628 reporterOptionsDivs
= "\n".join(reporterOptions
)
629 reportersArray
= "[%s]" % (
630 ",".join([repr(r
.getName()) for r
in self
.server
.reporters
])
634 fieldSize
= min(5, len(c
.files
))
635 attachFileOptions
= "\n".join(
638 <option value="%d" selected>%s</option>"""
640 for i
, v
in enumerate(c
.files
)
645 <td class="form_label">Attach:</td>
646 <td class="form_value">
647 <select style="width:100%%" name="files" multiple size=%d>
653 min(5, len(c
.files
)),
662 <title>File Bug</title>
663 <link rel="stylesheet" type="text/css" href="/scanview.css" />
665 <script language="javascript" type="text/javascript">
666 var reporters = %(reportersArray)s;
667 function updateReporterOptions() {
668 index = document.getElementById('reporter').selectedIndex;
669 for (var i=0; i < reporters.length; ++i) {
670 o = document.getElementById(reporters[i] + "ReporterOptions");
672 o.style.display = "";
674 o.style.display = "none";
679 <body onLoad="updateReporterOptions()">
681 <a href="/">Summary</a> >
684 <form name="form" action="/report_submit" method="post">
685 <input type="hidden" name="report" value="%(report)s">
689 <table class="form_group">
691 <td class="form_clabel">Title:</td>
692 <td class="form_value">
693 <input type="text" name="title" size="50" value="%(title)s">
697 <td class="form_label">Description:</td>
698 <td class="form_value">
699 <textarea rows="10" cols="80" name="description">
709 <table class="form_group">
711 <td class="form_clabel">Method:</td>
712 <td class="form_value">
713 <select id="reporter" name="reporter" onChange="updateReporterOptions()">
714 %(reporterSelections)s
718 %(reporterOptionsDivs)s
722 <tr><td class="form_submit">
723 <input align="right" type="submit" name="Submit" value="Submit">
735 return self
.send_string(result
)
737 def send_head(self
, fields
=None):
738 if self
.server
.options
.onlyServeLocal
and self
.client_address
[0] != "127.0.0.1":
739 return self
.send_error(401, "Unauthorized host.")
745 o
= urlparse(self
.path
)
746 self
.fields
= parse_query(o
.query
, fields
)
747 path
= posixpath
.normpath(unquote(o
.path
))
749 # Split the components and strip the root prefix.
750 components
= path
.split("/")[1:]
752 # Special case some top-level entries.
755 if len(components
) == 2:
757 return self
.send_report(components
[1])
759 return self
.send_open_report(components
[1])
760 elif len(components
) == 1:
763 return self
.send_string("Goodbye.", "text/plain")
764 elif name
== "report_submit":
765 return self
.send_report_submit()
766 elif name
== "report_crashes":
767 overrides
= {"ScanView": {}, "Radar": {}, "Email": {}}
768 for i
, r
in enumerate(self
.server
.reporters
):
769 if r
.getName() == "Radar":
770 overrides
["ScanView"]["reporter"] = i
772 overrides
["Radar"]["Component"] = "llvm - checker"
773 overrides
["Radar"]["Component Version"] = "X"
774 return self
.send_report(None, overrides
)
775 elif name
== "favicon.ico":
776 return self
.send_path(posixpath
.join(kShare
, "bugcatcher.ico"))
778 # Match directory entries.
779 if components
[-1] == "":
780 components
[-1] = "index.html"
782 relpath
= "/".join(components
)
783 path
= posixpath
.join(self
.server
.root
, relpath
)
785 if self
.server
.options
.debug
> 1:
787 '%s: SERVER: sending path "%s"' % (sys
.argv
[0], path
), file=sys
.stderr
789 return self
.send_path(path
)
792 self
.send_error(404, "File not found")
795 def send_path(self
, path
):
796 # If the requested path is outside the root directory, do not open it
797 rel
= os
.path
.abspath(path
)
798 if not rel
.startswith(os
.path
.abspath(self
.server
.root
)):
799 return self
.send_404()
801 ctype
= self
.guess_type(path
)
802 if ctype
.startswith("text/"):
804 return self
.send_patched_file(path
, ctype
)
810 return self
.send_404()
811 return self
.send_file(f
, ctype
)
813 def send_file(self
, f
, ctype
):
814 # Patch files to add links, but skip binary files.
815 self
.send_response(200)
816 self
.send_header("Content-type", ctype
)
817 fs
= os
.fstat(f
.fileno())
818 self
.send_header("Content-Length", str(fs
[6]))
819 self
.send_header("Last-Modified", self
.date_time_string(fs
.st_mtime
))
823 def send_string(self
, s
, ctype
="text/html", headers
=True, mtime
=None):
824 encoded_s
= s
.encode("utf-8")
826 self
.send_response(200)
827 self
.send_header("Content-type", ctype
)
828 self
.send_header("Content-Length", str(len(encoded_s
)))
830 mtime
= self
.dynamic_mtime
831 self
.send_header("Last-Modified", self
.date_time_string(mtime
))
833 return BytesIO(encoded_s
)
835 def send_patched_file(self
, path
, ctype
):
836 # Allow a very limited set of variables. This is pretty gross.
838 variables
["report"] = ""
839 m
= kReportFileRE
.match(path
)
841 variables
["report"] = m
.group(2)
846 return self
.send_404()
847 fs
= os
.fstat(f
.fileno())
848 data
= f
.read().decode("utf-8")
849 for a
, b
in kReportReplacements
:
850 data
= a
.sub(b
% variables
, data
)
851 return self
.send_string(data
, ctype
, mtime
=fs
.st_mtime
)
854 def create_server(address
, options
, root
):
857 reporters
= Reporter
.getReporters()
859 return ScanViewServer(address
, ScanViewRequestHandler
, root
, reporters
, options
)