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:interruptedupon 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.WatchedFilei.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 ofwatchedfilesto check for changes, you won't want this to be too small and it's lower bounded to0.05inSimpleWatcher.
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.statusshould be set to:interruptedso 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.taskis 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
endThat 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),
FileWatchingis 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.