@@ 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)
@@ 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>