~sqwishy/froghat.ca

b2608d1ae885a8ca1898a5c4695bf7311f53d888 — sqwishy 3 months ago 8799d7c
reload moved from template to http handler
3 files changed, 70 insertions(+), 34 deletions(-)

M fucko/boingo.py
R templates/partials/hot.html => fucko/hot.js
M templates/base.html
M fucko/boingo.py => fucko/boingo.py +63 -16
@@ 156,8 156,13 @@ def serve(host, port, show):
                rebuild_at = None

                rebuild_subp = ninja_subprocess(generate_ninja_build())
                rebuild_fd = open(os.pidfd_open(rebuild_subp.pid), closefd=True)
                rebuild_key = selector.register(rebuild_fd, selectors.EVENT_READ)
                try:
                    rebuild_fd = open(os.pidfd_open(rebuild_subp.pid), closefd=True)
                    rebuild_key = selector.register(rebuild_fd, selectors.EVENT_READ)
                except OsError as err:
                    logger.warning("could not wait on ninja subprocess, maybe it already finished?", err=err)
                    rebuild_fd = None
                    rebuild_key = None

            timeout = None if rebuild_in is None else rebuild_in / NS_IN_S
            try:


@@ 232,35 237,77 @@ def start_httpd(host, port, srv, rebuild_continuum):

    ServerClass.address_family, addr = _get_best_family(host, port)

    with open(project_root / "fucko/hot.js", "rb") as js_file:
        # this is loaded when we start serving, if it changes we don't notice :(
        HOT = b"<script>" + js_file.read() + b"</script>"

    class HandlerClass(SimpleHTTPRequestHandler):
        protocol_version = "HTTP/1.1"

        def __init__(self, *args, **kwargs):
            super().__init__(*args, directory=srv, **kwargs)

        def handle_one_request(self):
            # self.is_hot tells us this request gets hot-reload JavaScript stuff added
            # to the response, but since this instance handles multiple requests, reset
            # it here instead of in __init__
            self.is_hot = False
            return super().handle_one_request()

        def do_GET(self):
            if self.path.startswith("/_watch/"):
                self.send_response(HTTPStatus.OK)
                self.send_header("cache-control", "no-store")
                self.send_header("content-type", "text/event-stream")
                self.end_headers()

                watchlist = watch_filepaths(self.path)
                logger.debug("watching", sock=self.wfile, watch=watchlist)

                for data in watch_file_events(watchlist, self.wfile):
                    logger.debug("watch event", data=data)
                    self.wfile.write(f"data: {data}\n\n".encode())
                self.send_watch_events_forever()
            else:
                super().do_GET()

        def send_response(self, code, *args, **kwargs):
            super().send_response(code, *args, **kwargs)
        def send_response(self, code):
            super().send_response(code)
            if code == 301:
                # Stupid hack to prevent the client from waiting for a response
                # body forever in http/1.1.
                self.send_header("content-length", "0")

        def send_watch_events_forever(self):
            self.send_response(HTTPStatus.OK)
            self.send_header("cache-control", "no-store")
            self.send_header("content-type", "text/event-stream")
            self.end_headers()

            watchlist = watch_filepaths(self.path)
            logger.debug("watching", sock=self.wfile, watch=watchlist)

            for data in watch_file_events(watchlist, self.wfile):
                logger.debug("watch event", data=data)
                self.wfile.write(f"data: {data}\n\n".encode())

        # Automatic reload is done by appending some JavaScript to the end of
        # http responses to text/html files.
        #
        # For this, we reimplement SimpleHTTPRequestHandler.copyfile() to append the
        # JavaScript and SimpleHTTPRequestHandler.send_header() to adjust the
        # content-length appropriately.
        #
        # We also decide to append the JavaScript based on the value of the content-type
        # header sent in send_header().  So we assume that the content-type header is
        # sent before content-length.

        def send_header(self, name, value):
            name = name.lower()

            if not self.is_hot and "content-type" == name:
                self.is_hot = value == "text/html"

            elif self.is_hot and "content-length" == name.lower():
                value = str(int(value) + len(HOT))

            return super().send_header(name, value)

        def copyfile(self, source, outputfile):
            super().copyfile(source, outputfile)

            if self.is_hot:
                outputfile.write(HOT)

        def log_message(self, format, *args):
            logger.debug(format, *args)



@@ 279,7 326,7 @@ def start_httpd(host, port, srv, rebuild_continuum):
    def watch_file_events(watchlist, sock):
        import select

        changes = Debounce(ms=10)
        changes = Debounce(ms=20)

        with select.epoll() as epoll, rebuild_continuum.queue() as rebuilds:
            epoll.register(sock, select.EPOLLHUP)

R templates/partials/hot.html => fucko/hot.js +6 -8
@@ 1,9 1,9 @@
<script>
const watch = [
/* reload on source change / zoomer shit */
const watchlist = [
    window.location.pathname,
    "/static/css/meme.css",
];
const qs = watch.map(encodeURIComponent).join("&")
const qs = watchlist.map(encodeURIComponent).join("&")
const events = new EventSource("/_watch/?" + qs);

events.addEventListener("message", event => {


@@ 12,13 12,12 @@ events.addEventListener("message", event => {
    if (path.endsWith(".html")) {
        window.location.reload()

    } else if (path.endsWith(".css")) {
        /* I'm assuming the first match is the one we want to refresh */
        let link = document.head.querySelector('link[rel=stylesheet]');
    } else if (path.endsWith("meme.css")) {
        let link = document.head.querySelector('link[rel=stylesheet][href*="meme.css"]');
        let parent = link.parentElement;

        let clone = link.cloneNode();
        clone.href += "?"; /* TODO */
        clone.href = clone.href.replace(/\?.*|$/, "?" + Date.now());

        /* insert the new stylesheet link but do not remove the current one until the
        new one has loaded to avoid unstyled blinkyness */


@@ 29,4 28,3 @@ events.addEventListener("message", event => {
        console.warning("unhandled watch event", event);
    }
});
</script>

M templates/base.html => templates/base.html +1 -10
@@ 45,14 45,5 @@
    %   endif
    % endfor
</head>
<body>\
<%block name="stuff"/>\
## a cheap way to differentiate serve from build, it's preferred to inject this in
## the responses that serve() sends but modifying the http server to do that is kind
## of nasty, this is a temporary (tm) hack but it kind of sucks because command line
## config changes specified with -c don't trigger rebuilds
% if not SITE.url:
<%include file="partials/hot.html"/>
% endif
</body>
<body><%block name="stuff"/></body>
</html>