Skip to content

Commit

Permalink
fixes for #160
Browse files Browse the repository at this point in the history
  • Loading branch information
tlienart committed May 4, 2023
1 parent 36a7b20 commit f942613
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 191 deletions.
90 changes: 48 additions & 42 deletions src/file_watching.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""
WatchedFile
Struct for a file being watched containing the path to the file as well as the time of last
modification.
Struct for a file being watched containing the path to the file as well as the
time of last modification.
"""
mutable struct WatchedFile{T<:AbstractString}
path::T
Expand All @@ -15,15 +15,15 @@ end
Construct a new `WatchedFile` object around a file `f_path`.
"""
WatchedFile(f_path::AbstractString) = WatchedFile(f_path, mtime(f_path))


"""
has_changed(wf::WatchedFile)
Check if a `WatchedFile` has changed. Returns -1 if the file does not exist, 0 if it does exist but
has not changed, and 1 if it has changed.
Check if a `WatchedFile` has changed. Returns -1 if the file does not exist, 0
if it does exist but has not changed, and 1 if it has changed.
"""
function has_changed(wf::WatchedFile)
function has_changed(wf::WatchedFile)::Int
if !isfile(wf.path)
# isfile may return false for a file
# currently being written. Wait for 0.1s
Expand All @@ -39,7 +39,7 @@ end
Set the current state of a `WatchedFile` as unchanged"
"""
set_unchanged!(wf::WatchedFile) = (wf.mtime = mtime(wf.path))
set_unchanged!(wf::WatchedFile) = (wf.mtime = mtime(wf.path);)


"""
Expand All @@ -53,31 +53,42 @@ abstract type FileWatcher end
"""
SimpleWatcher([callback]; sleeptime::Float64=0.1) <: FileWatcher
A simple file watcher. You can specify a callback function, receiving the path of each file that
has changed as an `AbstractString`, at construction or later by the API function [`set_callback!`](@ref).
The `sleeptime` is the time waited between two runs of the loop looking for changed files, it is
constrained to be at least 0.05s.
A simple file watcher. You can specify a callback function, receiving the path
of each file that has changed as an `AbstractString`, at construction or later
by the API function [`set_callback!`](@ref).
The `sleeptime` is the time waited between two runs of the loop looking for
changed files, it is constrained to be at least 0.05s.
"""
mutable struct SimpleWatcher <: FileWatcher
callback::Union{Nothing,Function} # callback function triggered upon file change
callback::Union{Nothing,Function} # callback triggered upon file change
task::Union{Nothing,Task} # asynchronous file-watching task
sleeptime::Float64 # sleep-time before checking for file changes
sleeptime::Float64 # sleep before checking for file changes
watchedfiles::Vector{WatchedFile} # list of files being watched
status::Symbol # set to :interrupted as appropriate (caught by server)
status::Symbol # flag caught by server
end

SimpleWatcher(callback::Union{Nothing,Function}=nothing; sleeptime::Float64=0.1) =
SimpleWatcher(callback, nothing, max(0.05, sleeptime), Vector{WatchedFile}(), :runnable)
function SimpleWatcher(
callback::Union{Nothing,Function}=nothing;
sleeptime::Float64=0.1
)
return SimpleWatcher(
callback,
nothing,
max(0.05, sleeptime),
Vector{WatchedFile}(),
:runnable
)
end


"""
file_watcher_task!(w::FileWatcher)
Helper function that's spawned as an asynchronous task and checks for file changes. This task
is normally terminated upon an `InterruptException` and shows a warning in the presence of
any other exception.
Helper function that's spawned as an asynchronous task and checks for file
changes. This task is normally terminated upon an `InterruptException` and
shows a warning in the presence of any other exception.
"""
function file_watcher_task!(fw::FileWatcher)
function file_watcher_task!(fw::FileWatcher)::Nothing
try
while true
sleep(fw.sleeptime)
Expand All @@ -89,20 +100,10 @@ function file_watcher_task!(fw::FileWatcher)
deleted_files = Vector{Int}()
for (i, wf) enumerate(fw.watchedfiles)
state = has_changed(wf)
if state == 0
continue
elseif state == 1
# the file has changed, set it unchanged and trigger callback
if state == 1
# file has changed, set it unchanged and trigger callback
set_unchanged!(wf)
fw.callback(wf.path)
elseif state == -1
# the file does not exist, eventually delete it from list of watched files
push!(deleted_files, i)
if VERBOSE[]
@info "[FileWatcher]: file '$(wf.path)' does not " *
"exist (anymore); removing it from list of " *
" watched files."
end
end
end
# remove deleted files from list of watched files
Expand All @@ -114,8 +115,8 @@ function file_watcher_task!(fw::FileWatcher)
if !isa(err, InterruptException) && VERBOSE[]
@error "fw error" exception=(err, catch_backtrace())
end
return nothing
end
return nothing
end


Expand All @@ -125,7 +126,7 @@ end
Set or change the callback function being executed upon a file change.
Can be "hot-swapped", i.e. while the file watcher is running.
"""
function set_callback!(fw::FileWatcher, callback::Function)
function set_callback!(fw::FileWatcher, callback::Function)::Nothing
prev_running = stop(fw) # returns true if was running
fw.callback = callback
prev_running && start(fw) # restart if it was running before
Expand All @@ -149,7 +150,8 @@ Start the file watcher and wait to make sure the task has started.
"""
function start(fw::FileWatcher)
is_running(fw) || (fw.task = @async file_watcher_task!(fw))
# wait until task runs to ensure reliable start (e.g. if `stop` called right afterwards)
# wait until task runs to ensure reliable start (e.g. if `stop` called
# right after start)
while fw.task.state != :runnable
sleep(0.01)
end
Expand All @@ -159,15 +161,16 @@ end
"""
stop(fw::FileWatcher)
Stop the file watcher. The list of files being watched is preserved and new files can still be
added to the file watcher using `watch_file!`. It can be restarted with `start`.
Returns a `Bool` indicating whether the watcher was running before `stop` was called.
Stop the file watcher. The list of files being watched is preserved and new
files can still be added to the file watcher using `watch_file!`. It can be
restarted with `start`. Returns a `Bool` indicating whether the watcher was
running before `stop` was called.
"""
function stop(fw::FileWatcher)
function stop(fw::FileWatcher)::Bool
was_running = is_running(fw)
if was_running
# this may fail as the task may get interrupted in between which would lead to
# an error "schedule Task not runnable"
# this may fail as the task may get interrupted in between which would
# lead to an error "schedule Task not runnable"
try
schedule(fw.task, InterruptException(), error=true)
catch
Expand Down Expand Up @@ -198,6 +201,9 @@ Add a file to be watched for changes.
function watch_file!(fw::FileWatcher, f_path::AbstractString)
if isfile(f_path) && !is_watched(fw, f_path)
push!(fw.watchedfiles, WatchedFile(f_path))
VERBOSE[] && @info("[FileWatcher]: now watching '$f_path'")
if VERBOSE[]
@info "[FileWatcher]: now watching '$f_path'"
println()
end
end
end
Loading

0 comments on commit f942613

Please sign in to comment.