From 7cfbcae6850fafbe5e3069899b90d33d05857cb7 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Sun, 13 May 2018 21:23:57 +1000 Subject: [PATCH] webserver: Added a simple webserver to serve a project's reports. - e.g. sudo -H vigil-webserver my_project then see http://localhost:80 --- setup.py | 3 +- vigil/ColorMap.py | 128 +++++++++++++++++++++++++++++++++++++++++++++ vigil/__main__.py | 22 ++++++++ vigil/fill3.py | 11 ++++ vigil/termstr.py | 43 +++++++++++++-- vigil/tools.py | 6 +++ vigil/webserver.py | 95 +++++++++++++++++++++++++++++++++ 7 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 vigil/ColorMap.py create mode 100755 vigil/webserver.py diff --git a/setup.py b/setup.py index 81afe01..1dfa576 100755 --- a/setup.py +++ b/setup.py @@ -21,4 +21,5 @@ setup(name="vigil", package_data={"vigil": ["LS_COLORS.sh", "tools.yaml"]}, entry_points={"console_scripts": ["vigil=vigil.__main__:entry_point", - "vigil-worker=vigil.worker:main"]}) + "vigil-worker=vigil.worker:main", + "vigil-webserver=vigil.webserver:main"]}) diff --git a/vigil/ColorMap.py b/vigil/ColorMap.py new file mode 100644 index 0000000..20df651 --- /dev/null +++ b/vigil/ColorMap.py @@ -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 diff --git a/vigil/__main__.py b/vigil/__main__.py index 9e2fadf..bed5e83 100755 --- a/vigil/__main__.py +++ b/vigil/__main__.py @@ -117,6 +117,18 @@ class Entry(collections.UserList): self.appearance_cache = appearance = new_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): return any(part.startswith(".") for part in path.split(os.path.sep)) @@ -420,6 +432,16 @@ class Summary: if result.tool == tool: 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 ("
" +
+                "
".join(html_parts) + "
"), styles + class Log: diff --git a/vigil/fill3.py b/vigil/fill3.py index ecd2dac..59e1b06 100644 --- a/vigil/fill3.py +++ b/vigil/fill3.py @@ -72,6 +72,17 @@ def even_widths(column_widgets, width): 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
" + "
".join(lines) + "
") + + class Row(collections.UserList): def __init__(self, widgets, widths_func=even_widths): diff --git a/vigil/termstr.py b/vigil/termstr.py index c94fd4d..8982211 100644 --- a/vigil/termstr.py +++ b/vigil/termstr.py @@ -3,15 +3,26 @@ # Licensed under the Artistic License 2.0. import collections +import functools +import html import itertools import os import weakref import pygments.formatters.terminal256 +import vigil.ColorMap 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 decorator(self, *args, **kwds): try: @@ -102,6 +113,20 @@ class CharStyle: return "".join([terminal.normal, fg_termcode, bg_termcode, bold_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 ("" % + (id(self), fg_color, bg_color, bold_code, + italic_code, underline_code)) + def _join_lists(lists): return list(itertools.chain.from_iterable(lists)) @@ -110,10 +135,9 @@ def _join_lists(lists): class TermStr(collections.UserString): def __init__(self, data, style=CharStyle()): - if isinstance(data, self.__class__): - self.data = data.data - self.style = data.style - else: + try: + self.data, self.style = data.data, data.style + except AttributeError: self.data = data self.style = (style if isinstance(style, tuple) else (style,) * len(data)) @@ -262,3 +286,14 @@ class TermStr(collections.UserString): return CharStyle(style.fg_color, bg_color, is_bold=style.is_bold, is_underlined=style.is_underlined) 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('%s' % (id(style), encoded)) + return "".join(result), styles diff --git a/vigil/tools.py b/vigil/tools.py index 3e6b3d7..f3117d5 100644 --- a/vigil/tools.py +++ b/vigil/tools.py @@ -771,6 +771,12 @@ class Result: def appearance_min(self): return [status_to_str(self.status)] + def as_html(self): + html, styles = termstr.TermStr(status_to_str(self.status)).as_html() + return ('%s' % + (self.tool.__name__, self.path, self.tool.__name__, html), + styles) + def generic_tools(): return [contents, metadata] diff --git a/vigil/webserver.py b/vigil/webserver.py new file mode 100755 index 0000000..46139cb --- /dev/null +++ b/vigil/webserver.py @@ -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 + +Example: + vigil-webserver my_project +""" + + +def make_page(body_html, title): + return ("%s" + "%s" + % (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()