webserver: Added a simple webserver to serve a project's reports.

- e.g. sudo -H vigil-webserver my_project
  then see http://localhost:80
This commit is contained in:
Andrew Hamilton 2018-05-13 21:23:57 +10:00
parent 748f6f932b
commit 7cfbcae685
7 changed files with 303 additions and 5 deletions

View file

@ -21,4 +21,5 @@ setup(name="vigil",
package_data={"vigil": ["LS_COLORS.sh", "tools.yaml"]}, package_data={"vigil": ["LS_COLORS.sh", "tools.yaml"]},
entry_points={"console_scripts": entry_points={"console_scripts":
["vigil=vigil.__main__:entry_point", ["vigil=vigil.__main__:entry_point",
"vigil-worker=vigil.worker:main"]}) "vigil-worker=vigil.worker:main",
"vigil-webserver=vigil.webserver:main"]})

128
vigil/ColorMap.py Normal file
View file

@ -0,0 +1,128 @@
# This is from https://github.com/broadinstitute/xtermcolor
# Copyright (C) 2012 The Broad Institute
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class TerminalColorMapException(Exception):
pass
def _rgb(color):
return ((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff)
def _diff(color1, color2):
(r1, g1, b1) = _rgb(color1)
(r2, g2, b2) = _rgb(color2)
return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2)
class TerminalColorMap:
def getColors(self, order='rgb'):
return self.colors
def convert(self, hexcolor):
diffs = {}
for xterm, rgb in self.colors.items():
diffs[_diff(rgb, hexcolor)] = xterm
minDiffAnsi = diffs[min(diffs.keys())]
return (minDiffAnsi, self.colors[minDiffAnsi])
def colorize(self, string, rgb=None, ansi=None, bg=None, ansi_bg=None):
'''Returns the colored string'''
if not isinstance(string, str):
string = str(string)
if rgb is None and ansi is None:
raise TerminalColorMapException(
'colorize: must specify one named parameter: rgb or ansi')
if rgb is not None and ansi is not None:
raise TerminalColorMapException(
'colorize: must specify only one named parameter: rgb or ansi')
if bg is not None and ansi_bg is not None:
raise TerminalColorMapException(
'colorize: must specify only one named parameter: bg or ansi_bg')
if rgb is not None:
(closestAnsi, closestRgb) = self.convert(rgb)
elif ansi is not None:
(closestAnsi, closestRgb) = (ansi, self.colors[ansi])
if bg is None and ansi_bg is None:
return "\033[38;5;{ansiCode:d}m{string:s}\033[0m".format(ansiCode=closestAnsi, string=string)
if bg is not None:
(closestBgAnsi, unused) = self.convert(bg)
elif ansi_bg is not None:
(closestBgAnsi, unused) = (ansi_bg, self.colors[ansi_bg])
return "\033[38;5;{ansiCode:d}m\033[48;5;{bf:d}m{string:s}\033[0m".format(ansiCode=closestAnsi, bf=closestBgAnsi, string=string)
class VT100ColorMap(TerminalColorMap):
primary = [
0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0
]
bright = [
0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff
]
def __init__(self):
self.colors = dict()
self._compute()
def _compute(self):
for index, color in enumerate(self.primary + self.bright):
self.colors[index] = color
class XTermColorMap(VT100ColorMap):
grayscale_start = 0x08
grayscale_end = 0xf8
grayscale_step = 10
intensities = [
0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF
]
def _compute(self):
for index, color in enumerate(self.primary + self.bright):
self.colors[index] = color
c = 16
for i in self.intensities:
color = i << 16
for j in self.intensities:
color &= ~(0xff << 8)
color |= j << 8
for k in self.intensities:
color &= ~0xff
color |= k
self.colors[c] = color
c += 1
c = 232
for hex in list(range(self.grayscale_start, self.grayscale_end, self.grayscale_step)):
color = (hex << 16) | (hex << 8) | hex
self.colors[c] = color
c += 1

View file

@ -117,6 +117,18 @@ class Entry(collections.UserList):
self.appearance_cache = appearance = new_appearance self.appearance_cache = appearance = new_appearance
return appearance return appearance
def as_html(self):
html_parts = []
styles = set()
for result in self.widget:
result_html, result_styles = result.as_html()
html_parts.append(result_html)
styles.update(result_styles)
path = tools.path_colored(self.path)
padding = " " * (self.summary._max_width - len(self.widget) + 1)
path_html, path_styles = termstr.TermStr(padding + path).as_html()
return "".join(html_parts) + path_html, styles.union(path_styles)
def is_path_excluded(path): def is_path_excluded(path):
return any(part.startswith(".") for part in path.split(os.path.sep)) return any(part.startswith(".") for part in path.split(os.path.sep))
@ -420,6 +432,16 @@ class Summary:
if result.tool == tool: if result.tool == tool:
self.refresh_result(result) self.refresh_result(result)
def as_html(self):
html_parts = []
styles = set()
for row in self._column:
html_row, styles_row = row.as_html()
html_parts.append(html_row)
styles.update(styles_row)
return ("<style>a { text-decoration:none; }</style><pre>" +
"<br>".join(html_parts) + "</pre>"), styles
class Log: class Log:

View file

@ -72,6 +72,17 @@ def even_widths(column_widgets, width):
return widths return widths
def appearance_as_html(appearance):
lines = []
all_styles = set()
for line in appearance:
html, styles = termstr.TermStr(line).as_html()
all_styles.update(styles)
lines.append(html)
return ("\n".join(style.as_html() for style in all_styles) +
"\n<pre>" + "<br>".join(lines) + "</pre>")
class Row(collections.UserList): class Row(collections.UserList):
def __init__(self, widgets, widths_func=even_widths): def __init__(self, widgets, widths_func=even_widths):

View file

@ -3,15 +3,26 @@
# Licensed under the Artistic License 2.0. # Licensed under the Artistic License 2.0.
import collections import collections
import functools
import html
import itertools import itertools
import os import os
import weakref import weakref
import pygments.formatters.terminal256 import pygments.formatters.terminal256
import vigil.ColorMap
import vigil.terminal as terminal import vigil.terminal as terminal
xterm_colormap = vigil.ColorMap.XTermColorMap()
@functools.lru_cache()
def xterm_color_to_rgb(color_index):
return vigil.ColorMap._rgb(xterm_colormap.colors[color_index])
def _cache_first_result(user_function): def _cache_first_result(user_function):
def decorator(self, *args, **kwds): def decorator(self, *args, **kwds):
try: try:
@ -102,6 +113,20 @@ class CharStyle:
return "".join([terminal.normal, fg_termcode, bg_termcode, bold_code, return "".join([terminal.normal, fg_termcode, bg_termcode, bold_code,
italic_code, underline_code]) italic_code, underline_code])
def as_html(self):
bold_code = "font-weight:bold; " if self.is_bold else ""
italic_code = "font-style:italic; " if self.is_italic else ""
underline_code = ("text-decoration:underline; "
if self.is_underlined else "")
fg_color = (self.fg_color if type(self.fg_color) == tuple
else xterm_color_to_rgb(self.fg_color))
bg_color = (self.bg_color if type(self.bg_color) == tuple
else xterm_color_to_rgb(self.bg_color))
return ("<style>.S%i {font-size:80%%; color:rgb%r; "
"background-color:rgb%r; %s%s%s}</style>" %
(id(self), fg_color, bg_color, bold_code,
italic_code, underline_code))
def _join_lists(lists): def _join_lists(lists):
return list(itertools.chain.from_iterable(lists)) return list(itertools.chain.from_iterable(lists))
@ -110,10 +135,9 @@ def _join_lists(lists):
class TermStr(collections.UserString): class TermStr(collections.UserString):
def __init__(self, data, style=CharStyle()): def __init__(self, data, style=CharStyle()):
if isinstance(data, self.__class__): try:
self.data = data.data self.data, self.style = data.data, data.style
self.style = data.style except AttributeError:
else:
self.data = data self.data = data
self.style = (style if isinstance(style, tuple) self.style = (style if isinstance(style, tuple)
else (style,) * len(data)) else (style,) * len(data))
@ -262,3 +286,14 @@ class TermStr(collections.UserString):
return CharStyle(style.fg_color, bg_color, is_bold=style.is_bold, return CharStyle(style.fg_color, bg_color, is_bold=style.is_bold,
is_underlined=style.is_underlined) is_underlined=style.is_underlined)
return self.transform_style(set_bgcolor) return self.transform_style(set_bgcolor)
def as_html(self):
result = []
styles = set()
for style, str_, position in self._partition_style():
styles.add(style)
encoded = str(html.escape(str_).encode(
"ascii", "xmlcharrefreplace"))[2:-1]
encoded = encoded.replace("\\\\", "\\")
result.append('<span class="S%i">%s</span>' % (id(style), encoded))
return "".join(result), styles

View file

@ -771,6 +771,12 @@ class Result:
def appearance_min(self): def appearance_min(self):
return [status_to_str(self.status)] return [status_to_str(self.status)]
def as_html(self):
html, styles = termstr.TermStr(status_to_str(self.status)).as_html()
return ('<a title="%s" href="%s/%s">%s</a>' %
(self.tool.__name__, self.path, self.tool.__name__, html),
styles)
def generic_tools(): def generic_tools():
return [contents, metadata] return [contents, metadata]

95
vigil/webserver.py Executable file
View file

@ -0,0 +1,95 @@
#!/usr/bin/env python3.6
# Copyright (C) 2018 Andrew Hamilton. All rights reserved.
# Licensed under the Artistic License 2.0.
import gzip
import http.server
import os
import sys
import pickle
import vigil.fill3 as fill3
import vigil.tools as tools
USAGE = """Usage:
vigil-webserver <directory>
Example:
vigil-webserver my_project
"""
def make_page(body_html, title):
return ("<html><head><title>%s</title></head><body>"
"<style>body { background-color: black; } </style>%s</body></html>"
% (title, body_html)).encode("utf-8")
class Webserver(http.server.BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
def do_GET(self):
self._set_headers()
if self.path == "/":
page = summary_page
elif "/" in self.path[1:]:
path, tool = os.path.split(self.path[1:])
result = index[(path, tool)]
body = fill3.appearance_as_html(
fill3.Border(result).appearance_min())
page = make_page(body, "%s of %s" % (tool, path))
else:
return
self.wfile.write(page)
def do_HEAD(self):
self._set_headers()
def do_POST(self):
self._set_headers()
self.wfile.write("posted".encode("utf-8"))
def make_summary_page(project_name, summary):
summary_html, summary_styles = summary.as_html()
body_html = ("\n".join(style.as_html() for style in summary_styles)
+ "\n" + summary_html)
return make_page(body_html, "Summary of " + project_name)
def run(server_class=http.server.HTTPServer, handler_class=Webserver, port=80):
server_address = ("", port)
httpd = server_class(server_address, handler_class)
print("Starting httpd...")
httpd.serve_forever()
def main():
global summary_page, index
if len(sys.argv) == 1:
print(USAGE)
sys.exit(1)
project_path = os.path.abspath(sys.argv[1])
os.chdir(project_path)
project_name = os.path.basename(project_path)
pickle_path = os.path.join(project_path, tools.CACHE_PATH,
"summary.pickle")
with gzip.open(pickle_path, "rb") as file_:
screen = pickle.load(file_)
summary_page = make_summary_page(project_name, screen._summary)
index = {}
for row in screen._summary._column:
for result in row:
index[(result.path[2:], result.tool.__name__)] = result.result
run()
if __name__ == "__main__":
main()