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:
parent
748f6f932b
commit
7cfbcae685
7 changed files with 303 additions and 5 deletions
3
setup.py
3
setup.py
|
|
@ -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
128
vigil/ColorMap.py
Normal 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
|
||||||
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
95
vigil/webserver.py
Executable 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue