Like many software developers, I've occasionally needed an HTTP server for accessing files in a local directory. If you have Python installed, possibly the easiest way is to use the command python -m SimpleHTTPServer
, which I used as my default option for many years. When I started this blog, however, I wanted a server I could tweak easily, and I found this Babashka script. To provide a live-reload workflow, I tweaked it to hold onto a request until the file updated. Here are the essential changes.
The workhorse is a loop that checks the last time a file was updated, and if it's earlier than when the request was made, it waits and checks again:
(defn wait-for-update [f interval stop?]
(let [cutoff (.toEpochMilli (java.time.Instant/now))]
(loop []
(cond
@stop?
, false
(< cutoff (.lastModified (fs/file f)))
, true
:else
, (do
(Thread/sleep (* 1000 interval))
(recur))))))
The stop?
atom is used to quit looping if the client connection is closed. That's tripped by http-kit's as-channel
in the main request handler, using the :on-close
event:
(let [stop? (atom false)]
(server/as-channel request
{:on-close (fn [ch status]
(reset! stop? true))}))
By default, the server doesn't use wait-for-update
unless asked to. I trigger holding the request by adding the query parameter on-update
to requests:
(cond
...
(some->> (request :query-string)
(re-find #"on-update"))
, (do (wait-for-update f interval stop?)
(file f))
...)
It would be more appropriate to use an HTTP header, but when I tried I ran into Chrome's cache locking, which pauses requests made to the same URL as a pending request, and while Chrome does cancel the pending request after 20 seconds and let the subsequent request finish, that's a long time to wait for a page load that's otherwise instantaneous. The two proposed solutions don't seem to be practical for requests triggered by a browser refresh (adding a Cache-Control: no-store
header or including a unique query parameter). It's much easier to use a different URL for the long-polling request, which adding a query parameter accomplishes. Basic file servers like this don't typically need query parameters, so using ?on-update
hasn't interfered with anything I've used this script for.
The full script is here.
Beyond writing this blog, I've used this live-reload script for a couple other workflows, one of which I'll describe in a subsequent post.