Extending LiveServer
There may be circumstances where you will want the page-reloading to be triggered by your own mechanism. As a very simple example, you may want to display your own custom messages every time a file is updated. This page explains how to extend SimpleWatcher <: FileWatcher
and, more generally, how to write your own FileWatcher
. We also explain how in some circumstances it may be easier to feed a custom coreloopfun
to serve
rather than writing a custom callback.
Using SimpleWatcher
with a custom callback
In most circumstances, using an instance of the SimpleWatcher
type with your own custom callback function is what you will want to do.
The SimpleWatcher
does what you expect: it watches files for changes and triggers a function (the callback) when a change is detected. The callback function takes as argument the path of the file that was modified and returns nothing
.
The base callback function (LiveServer.file_changed_callback
) does only one thing: it sends a signal to the relevant viewers to trigger page reloads. You will typically want to re-use file_changed_callback
or copy its code.
As an example of a custom callback, here is a simple modified callback mechanism which prints Hello!
before using the base callback function:
custom_callback(fp::AbstractString) = (println("Hello!"); file_changed_callback(fp))
A more sophisticated customised callback is the one that is used in servedocs
(see LiveServer.servedocs_callback!
). The callback has a different behaviour depending on which file is modified and does a few extra steps before signalling the viewers to reload appropriate pages.
Writing your own FileWatcher
If you decide to write your own FileWatcher
type, you will need to meet the API. The easier is probably that you look at the code for LiveServer.SimpleWatcher
and adapt it to your need. Let's assume for now that you want to define a CustomWatcher <: FileWatcher
.
Fields
The only field that is required by the rest of the code is
status
: a symbol that must be set to:interrupted
upon errors in the file watching task
Likely you will want to have some (probably most) of the fields of a SimpleWatcher
i.e.:
callback
: the callback function to be triggered upon an event,task
: the asynchronous file watching task,watchedfiles
: the vector ofLiveServer.WatchedFile
i.e. the paths to the file being watched as well as their time of last modification,sleeptime
: the time to wait before going over the list ofwatchedfiles
to check for changes, you won't want this to be too small and it's lower bounded to0.05
inSimpleWatcher
.
Of course you can add any extra field you may want.
Methods
Subsequently, your CustomWatcher
may redefine some or all of the following methods (those that aren't will use the default method defined for FileWatcher
and thus all of its sub-types).
The methods that are required by the rest of the code are
start(::FileWatcher)
andstop(::FileWatcher)
to start and stop the watcher,watch_file!(::FileWatcher, ::AbstractString)
to consider an additional file.
You may also want to re-define existing methods such as
file_watcher_task!(::FileWatcher)
: the loop that goes over the watched files, checking for modifications and triggering the callback function. This task will be referenced by the fieldCustomWatcher.task
. If errors happen in this asynchronous task, theCustomWatcher.status
should be set to:interrupted
so that all running tasks can be stopped properly.set_callback!(::FileWatcher, ::Function)
: a helper function to bind a watcher with a callback function.is_running(::FileWatcher)
: a helper function to check whetherCustomWatcher.task
is done.is_watched(::FileWatcher, ::AbstractString)
: check if a file is watched by the watcher.
Using a custom coreloopfun
In some circumstances, your code may be using specific data structures or be such that it would not easily play well with a FileWatcher
mechanism. In that case, you may want to also specify a coreloopfun
which is called continuously from within the serve
main loop.
The code of serve
is essentially structured as follows:
function serve(...)
# ...
@async HTTP.listen(...) # handles messages with the client (browser)
# ...
try
counter = 1
while true # the main loop
# ...
coreloopfun(counter, filewatcher)
counter += 1
sleep(0.1)
end
catch err
# ...
finally
# cleanup ...
end
return nothing
end
That is, the coreloopfun
is called roughly every 100 ms while the server is running. By default the coreloopfun
does nothing.
An example where this mechanism could be used is when your code handles the processing of files from one format (say markdown) to HTML. You want the FileWatcher
to trigger browser reloads whenever new versions of these HTML files are produced. However, at the same time, you want another process to keep track of the markdown files and re-process them as they change. You can hook this second watcher into the core loop of LiveServer
using the coreloopfun
. An example for this use case is JuDoc.jl.
Why not use FileWatching
?
You may be aware of the FileWatching
module in Base
and may wonder why we did not just use that one. The main reasons we decided not to use it are:
- it triggers a lot: where our system only triggers the callback function upon saving a file (e.g. you modified the file and saved the modification),
FileWatching
is more sensitive (for instance it will trigger when you open the file), - it is somewhat harder to make your own custom mechanisms to fire page reloads.
So ultimately, our system can be seen as a poor man's implementation of FileWatching
that is robust, simple and easy to customise.