Skip to content

Commit

Permalink
fixes for 404 (#161)
Browse files Browse the repository at this point in the history
* fixes for #160

* fixing tests

* prep a patch release

* last fix

* mark re-appearing files as changed with -inf mtime
  • Loading branch information
tlienart authored May 17, 2023
1 parent 36a7b20 commit 08bf742
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 208 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ authors = [
"Jonas Asprion <jonas.asprion@gmx.ch",
"Thibaut Lienart <tlienart@me.com>"
]
version = "1.1.1"
version = "1.1.2"

[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Expand Down
113 changes: 64 additions & 49 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 @@ -37,9 +37,17 @@ end
"""
set_unchanged!(wf::WatchedFile)
Set the current state of a `WatchedFile` as unchanged"
Set the current state of a `WatchedFile` as unchanged
"""
set_unchanged!(wf::WatchedFile) = (wf.mtime = mtime(wf.path);)

"""
set_unchanged!(wf::WatchedFile)
Set the current state of a `WatchedFile` as deleted (if it re-appears it will
immediately be marked as changed and trigger the callback).
"""
set_unchanged!(wf::WatchedFile) = (wf.mtime = mtime(wf.path))
set_deleted!(wf::WatchedFile) = (wf.mtime = -Inf;)


"""
Expand All @@ -53,69 +61,70 @@ 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)

# only check files if there's a callback to call upon changes
fw.callback === nothing && continue

# keep track of any file that may have been deleted
deleted_files = Vector{Int}()
for (i, wf) enumerate(fw.watchedfiles)
for wf 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
# file has been deleted, set the mtime to -Inf so that
# if it re-appears then it's immediately marked as changed
set_deleted!(wf)
end
end
# remove deleted files from list of watched files
deleteat!(fw.watchedfiles, deleted_files)
end
catch err
fw.status = :interrupted
# an InterruptException is the normal way for this task to end
if !isa(err, InterruptException) && VERBOSE[]
@error "fw error" exception=(err, catch_backtrace())
end
return nothing
end
return nothing
end


Expand All @@ -125,7 +134,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 +158,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 +169,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 All @@ -186,8 +197,9 @@ end
Checks whether the file specified by `f_path` is being watched.
"""
is_watched(fw::FileWatcher, f_path::AbstractString) =
any(wf -> wf.path == f_path, fw.watchedfiles)
function is_watched(fw::FileWatcher, f_path::AbstractString)
return any(wf -> wf.path == f_path, fw.watchedfiles)
end


"""
Expand All @@ -198,6 +210,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

2 comments on commit 08bf742

@tlienart
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/83760

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.1.2 -m "<description of version>" 08bf7427432da6d6824ba530b7e0904ca391bc4d
git push origin v1.1.2

Please sign in to comment.