diff --git a/Project.toml b/Project.toml index 3864353..0b874ae 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,7 @@ authors = [ "Jonas Asprion " ] -version = "1.1.1" +version = "1.1.2" [deps] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" diff --git a/src/file_watching.jl b/src/file_watching.jl index 87f9ccc..5feb33b 100644 --- a/src/file_watching.jl +++ b/src/file_watching.jl @@ -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 @@ -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 @@ -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;) """ @@ -53,31 +61,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) @@ -85,28 +104,18 @@ function file_watcher_task!(fw::FileWatcher) # 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 @@ -114,8 +123,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 @@ -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 @@ -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 @@ -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 @@ -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 """ @@ -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 diff --git a/src/server.jl b/src/server.jl index 2e8fed2..2cbd642 100644 --- a/src/server.jl +++ b/src/server.jl @@ -5,6 +5,7 @@ function detectwsl() occursin(r"Microsoft|WSL"i, read("/proc/sys/kernel/osrelease", String)) end + """ open_in_default_browser(url) @@ -26,11 +27,12 @@ function open_in_default_browser(url::AbstractString)::Bool else false end - catch ex + catch false end end + """ update_and_close_viewers!(wss::Vector{HTTP.WebSockets.WebSocket}) @@ -39,15 +41,18 @@ send a message with data "update" to each of them (to trigger a page reload), then close the connection. Finally, empty the list since all connections are closing anyway and clients will re-connect from the re-loaded page. """ -function update_and_close_viewers!(wss::Vector{HTTP.WebSockets.WebSocket}) +function update_and_close_viewers!( + wss::Vector{HTTP.WebSockets.WebSocket} + )::Nothing + ws_to_update_and_close = collect(wss) empty!(wss) # send update message to all viewers - @sync for wsi in ws_to_update_and_close - isopen(wsi.io) && @async begin + @sync for wsᵢ in ws_to_update_and_close + isopen(wsᵢ.io) && @async begin try - HTTP.WebSockets.send(wsi, "update") + HTTP.WebSockets.send(wsᵢ, "update") catch end end @@ -75,7 +80,7 @@ end Function reacting to the change of the file at `f_path`. Is set as callback for the file watcher. """ -function file_changed_callback(f_path::AbstractString) +function file_changed_callback(f_path::AbstractString)::Nothing if VERBOSE[] @info "[LiveServer]: Reacting to change in file '$f_path'..." end @@ -95,58 +100,97 @@ end """ - get_fs_path(req_path::AbstractString) + get_fs_path(req_path::AbstractString; silent=false) Return the filesystem path corresponding to a requested path, or an empty String if the file was not found. -Cases: - * an explicit request to an existing `index.html` (e.g. `foo/bar/index.html`) - is given --> serve the page and change WEB_DIR unless a parent dir should - be preferred (e.g. foo/ has an index.html) - * an implicit request to an existing `index.html` (e.g. `foo/bar/` or `foo/bar`) - is given --> same as previous case after appending the `index.html` - * a request to a file is given (e.g. `/sample.jpeg`) --> figure out what it - is relative to, reconstruct the full system path and serve the file - * a request for a dir without index is given (e.g. `foo/bar`) --> serve a +### Cases: +* an explicit request to an existing `index.html` (e.g. + `foo/bar/index.html`) is given --> serve the page and change WEB_DIR + unless a parent dir should be preferred (e.g. foo/ has an index.html) +* an implicit request to an existing `index.html` (e.g. `foo/bar/` or + `foo/bar`) is given --> same as previous case after appending the + `index.html` +* a request to a file is given (e.g. `/sample.jpeg`) --> figure out what it + is relative to, reconstruct the full system path and serve the file +* a request for a dir without index is given (e.g. `foo/bar`) --> serve a dedicated index file listing the content of the directory. """ -function get_fs_path(req_path::AbstractString)::String +function get_fs_path( + req_path::AbstractString; + silent::Bool=false, + onlyfs::Bool=false + ) + uri = HTTP.URI(req_path) r_parts = HTTP.URIs.unescapeuri.(split(lstrip(uri.path, '/'), '/')) fs_path = joinpath(CONTENT_DIR[], r_parts...) - resolved_fs_path = "" + onlyfs && return fs_path, :onlyfs + cand_index = ifelse( r_parts[end] == "index.html", fs_path, joinpath(fs_path, "index.html") ) + resolved_fs_path = "" + case = :undecided + if isfile(cand_index) resolved_fs_path = cand_index + case = :dir_with_index elseif isfile(fs_path) resolved_fs_path = fs_path + case = :file elseif isdir(fs_path) resolved_fs_path = joinpath(fs_path, "") + case = :dir_without_index + + elseif req_path == "/" + resolved_fs_path = "." + case = :dir_without_index + + else + for cand_404 in ( + joinpath(CONTENT_DIR[], "404.html"), + joinpath(CONTENT_DIR[], "404", "index.html") + ) + if isfile(cand_404) + resolved_fs_path = cand_404 + case = :not_found_with_404 + break + end + end + if isempty(resolved_fs_path) + case = :not_found_without_404 + end end - DEBUG[] && @info """ - 👀 RESOLVE (req: $req_path) 👀 - fs_path: $(fs_path) ($(resolved_fs_path)) - """ - return resolved_fs_path + if DEBUG[] && !silent + @info """ + 👀 PATH RESOLUTION + request: < $req_path > + fs_path: < $fs_path > + resolved: < $resolved_fs_path > + case: < $case > + """ + println() + end + return resolved_fs_path, case end + """ lstrip_cdir(s) Discard the 'CONTENT_DIR' part (passed via `dir=...`) of a path. """ -function lstrip_cdir(s::AbstractString) +function lstrip_cdir(s::AbstractString)::String # we can't easily do a regex match here because CONTENT_DIR may # contain regex characters such as `+` or `-` ss = string(s) @@ -156,12 +200,13 @@ function lstrip_cdir(s::AbstractString) return string(lstrip(ss, ['/', '\\'])) end + """ - get_dir_list(dir::AbstractString) -> index_page::AbstractString + get_dir_list(dir::AbstractString) -> index_page::String -Generate list of content at path `dir`. +Generate a page which lists content at path `dir`. """ -function get_dir_list(dir::AbstractString) +function get_dir_list(dir::AbstractString)::String list = readdir(dir; join=true, sort=true) io = IOBuffer() sdir = dir @@ -224,7 +269,9 @@ function get_dir_list(dir::AbstractString) write(io, """
- 💻 LiveServer.jl + + 💻 LiveServer.jl + """ @@ -232,6 +279,7 @@ function get_dir_list(dir::AbstractString) return String(take!(io)) end + """ serve_file(fw, req::HTTP.Request; inject_browser_reload_script = true) @@ -256,7 +304,8 @@ whether they're already watched or not. Finally the file is served via a 200 and message is returned. """ function serve_file( - fw, req::HTTP.Request; + fw::FileWatcher, + req::HTTP.Request; inject_browser_reload_script::Bool = true, allow_cors::Bool = false )::HTTP.Response @@ -270,12 +319,7 @@ function serve_file( # foo/bar?search --> foo/bar/?search # foo/bar#anchor --> foo/bar/#anchor # - uri = HTTP.URI(req.target) - - DEBUG[] && @info """ - REQUEST ($(req.target)) - uri.path ($(uri.path)) - """ + uri = HTTP.URI(req.target) cand_dir = joinpath(CONTENT_DIR[], split(uri.path, '/')...) if !endswith(uri.path, "/") && isdir(cand_dir) @@ -287,52 +331,41 @@ function serve_file( end ret_code = 200 - fs_path = get_fs_path(req.target) - - DEBUG[] && @info """ - PATH RESOLUTION ($(req.target)) - fs_path: $(fs_path) [$(ifelse(isempty(fs_path), "❌ ❌ ❌ ", ""))] - """ - - # if get_fs_path returns an empty string, there's two cases: - # 1. [CASE 3] the path is a directory without an `index.html` --> list dir - # 2. [CASE 4] otherwise serve a 404 (see if there's a dedicated 404 path, - if isempty(fs_path) - - if req.target == "/" - index_page = get_dir_list(".") - return HTTP.Response(200, index_page) - end + fs_path, case = get_fs_path(req.target) + if case == :not_found_without_404 + return HTTP.Response(404, + """ +
+

404 Not Found

+

+ The requested URL [$(req.target)] does not correspond to a resource on the server. +

+

+ Perhaps you made a typo in the URL, or the URL corresponds to a file that has been + deleted or renamed. +

+

+ Home +

+
+ """ + ) ret_code = 404 - # Check if /404/ or /404.html exists and serve that as a body - for f in ("/404/", "/404.html") - maybe_path = get_fs_path(f) - if !isempty(maybe_path) - fs_path = maybe_path - break - end - end - - # If still not found a body, return a generic error message - if isempty(fs_path) - return HTTP.Response(404, """ - 404: file not found. Perhaps you made a typo in the URL, - or the requested file has been deleted or renamed. - """ - ) - end - end - - # [CASE 2] - if isdir(fs_path) + elseif case == :not_found_with_404 + ret_code = 404 + elseif case == :dir_without_index index_page = get_dir_list(fs_path) return HTTP.Response(200, index_page) end + # # In what follows, fs_path points to a file - # --> [CASE 1a] html-like: try to inject reload-script - # --> [CASE 1b] other: just get the browser to show it + # :dir_with_index + # :file + # :not_found_with_404 + # --> html-like: try to inject reload-script + # --> other: just get the browser to show it # ext = lstrip(last(splitext(fs_path)), '.') |> string content = read(fs_path, String) @@ -380,22 +413,26 @@ function serve_file( io = IOBuffer() write(io, SubString(content, 1:end_body)) write(io, BROWSER_RELOAD_SCRIPT) - write(io, SubString(content, nextind(content, end_body):lastindex(content))) + content_from = nextind(content, end_body) + content_to = lastindex(content) + write(io, SubString(content, content_from:content_to)) content = take!(io) end end range_match = match(r"bytes=(\d+)-(\d+)" , HTTP.header(req, "Range", "")) is_ranged = !isnothing(range_match) - headers = [ "Content-Type" => content_type, ] if is_ranged range = parse.(Int, range_match.captures) - push!(headers, "Content-Range" => "bytes $(range[1])-$(range[2])/$(binary_length(content))") - content = @view content[1+range[1]:1+range[2]] + push!(headers, + "Content-Range" => + "bytes $(range[1])-$(range[2])/$(binary_length(content))" + ) + content = @view content[1+range[1]:1+range[2]] ret_code = 206 end if allow_cors @@ -416,27 +453,50 @@ binary_length(s::AbstractString) = ncodeunits(s) binary_length(s::AbstractVector{UInt8}) = length(s) +function add_to_viewers(fs_path, ws) + if haskey(WS_VIEWERS, fs_path) + push!(WS_VIEWERS[fs_path], ws) + else + WS_VIEWERS[fs_path] = [ws] + end + return +end + + """ ws_tracker(ws::HTTP.WebSockets.WebSocket, target::AbstractString) Adds the websocket connection to the viewers in the global dictionary `WS_VIEWERS` to the entry corresponding to the targeted file. """ -function ws_tracker(ws::HTTP.WebSockets.WebSocket) - # NOTE: this file always exists because the query is - # generated just after serving it - fs_path = get_fs_path(ws.request.target) +function ws_tracker(ws::HTTP.WebSockets.WebSocket)::Nothing + # NOTE: unless we're in the case of a 404, this file always exists because + # the query is generated just after serving it; the 404 case will return an + # empty path. + fs_path, case = get_fs_path(ws.request.target, silent=true) + + if case in (:not_found_with_404, :not_found_without_404) + raw_fs_path, _ = get_fs_path(ws.request.target, onlyfs=true) + add_to_viewers(raw_fs_path, ws) + end # add to list of html files being "watched" if the file is already being # viewed, add ws to it (e.g. several tabs) otherwise add to dict - if haskey(WS_VIEWERS, fs_path) - push!(WS_VIEWERS[fs_path], ws) - else - WS_VIEWERS[fs_path] = [ws] + if case != :not_found_without_404 + add_to_viewers(fs_path, ws) end + # if DEBUG[] + # for (k, v) in WS_VIEWERS + # println("$k > $(length(v)) viewers") + # for (i, vi) in enumerate(v) + # println(" $i - $(vi.writeclosed)") + # end + # end + # end + try - # NOTE: browsers will drop idle websocket connections so this effectively + # NOTE: browsers will drop idle websocket connections so this # forces the websocket to stay open until it's closed by LiveServer (and # not by the browser) upon writing a `update` message on the websocket. # See update_and_close_viewers @@ -444,15 +504,16 @@ function ws_tracker(ws::HTTP.WebSockets.WebSocket) sleep(0.1) end catch err - # NOTE: there may be several sources of errors caused by the precise moment - # at which the user presses CTRL+C and after what events. In an ideal world - # we would check that none of these errors have another source but for now - # we make the assumption it's always the case (note that it can cause other - # errors than InterruptException, for instance it can cause errors due to - # stream not being available etc but these all have the same source). - # - We therefore do not propagate the error but merely store the information - # that there was a forcible interruption of the websocket so that the - # interruption can be guaranteed to be propagated. + # NOTE: there may be several sources of errors caused by the precise + # moment at which the user presses CTRL+C and after what events. In an + # ideal world we would check that none of these errors have another + # source but for now we make the assumption it's always the case (note + # that it can cause other errors than InterruptException, for instance + # it can cause errors due to stream not being available etc but these + # all have the same source). + # - We therefore do not propagate the error but merely store the + # information that there was a forcible interruption of the websocket + # so that the interruption can be guaranteed to be propagated. WS_INTERRUPT[] = true end return nothing @@ -462,33 +523,31 @@ end """ serve(filewatcher; ...) -Main function to start a server at `http://host:port` and render what is in the current -directory. (See also [`example`](@ref) for an example folder). +Main function to start a server at `http://host:port` and render what is in the +current directory. (See also [`example`](@ref) for an example folder). # Arguments - `filewatcher`: a file watcher implementing the API described for - [`SimpleWatcher`](@ref) (which also is the default) and - messaging the viewers (via WebSockets) upon detecting file - changes. - `port`: integer between 8000 (default) and 9000. - `dir`: string specifying where to launch the server if not the current +- `filewatcher`: a file watcher implementing the API described for + [`SimpleWatcher`](@ref) (which also is the default) messaging the viewers + (via WebSockets) upon detecting file changes. +- `port`: integer between 8000 (default) and 9000. +- `dir`: string specifying where to launch the server if not the current working directory. - `verbose`: boolean switch to make the server print information about file - changes and connections. - `debug`: bolean switch to make the server print debug messages. - `coreloopfun`: function which can be run every 0.1 second while the - liveserver is running; it takes two arguments: the cycle - counter and the filewatcher. By default the coreloop does - nothing. - `launch_browser`: boolean specifying whether to launch the ambient browser - at the localhost or not (default: false). - `allow_cors`: boolean allowing cross origin (CORS) requests to access the - server via the "Access-Control-Allow-Origin" header. - `preprocess_request`: function specifying the transformation of a request - before it is returned; its only argument is the - current request. - # Example +- `debug`: bolean switch to make the server print debug messages. +- `verbose`: boolean switch to make the server print information about file + changes and connections. +- `coreloopfun`: function which can be run every 0.1 second while the + server is running; it takes two arguments: the cycle counter and the + filewatcher. By default the coreloop does nothing. +- `launch_browser`: boolean specifying whether to launch the ambient browser + at the localhost or not (default: false). +`allow_cors`: boolean allowing cross origin (CORS) requests to access the + server via the "Access-Control-Allow-Origin" header. +`preprocess_request`: function specifying the transformation of a request + before it is returned; its only argument is the current request. + +# Example ```julia LiveServer.example() @@ -500,19 +559,19 @@ directory. (See also [`example`](@ref) for an example folder). page and show the changes. """ function serve( - fw::FileWatcher=SimpleWatcher(file_changed_callback); - # kwargs - host::String = "127.0.0.1", - port::Int = 8000, - dir::AbstractString = "", - verbose::Bool = false, - debug::Bool = false, - coreloopfun::Function = (c, fw)->nothing, - preprocess_request::Function = identity, - inject_browser_reload_script::Bool = true, - launch_browser::Bool = false, - allow_cors::Bool = false - )::Nothing + fw::FileWatcher=SimpleWatcher(file_changed_callback); + # kwargs + host::String = "127.0.0.1", + port::Int = 8000, + dir::AbstractString = "", + debug::Bool = false, + verbose::Bool = debug, + coreloopfun::Function = (c, fw)->nothing, + preprocess_request::Function = identity, + inject_browser_reload_script::Bool = true, + launch_browser::Bool = false, + allow_cors::Bool = false + )::Nothing 8000 ≤ port ≤ 9000 || throw( ArgumentError("The port must be between 8000 and 9000.") @@ -527,6 +586,7 @@ directory. (See also [`example`](@ref) for an example folder). set_content_dir(dir) end + # starts the file watcher start(fw) # make request handler @@ -556,7 +616,8 @@ directory. (See also [`example`](@ref) for an example folder). # the websocket handling or during the file watching) throw(InterruptException()) end - # do the auxiliary function if there is one (by default this does nothing) + # run the auxiliary function if there is one (by default this does + # nothing) coreloopfun(counter, fw) # update the cycle counter and sleep (yields to other threads) counter += 1 @@ -598,10 +659,22 @@ directory. (See also [`example`](@ref) for an example folder). return nothing end -function get_server(host, port, req_handler; incr=0) - if incr >= 10 - @error "couldn't find a free port in $incr tries" - end + +""" + get_server(host, port, req_handler; incr=0) + +Helper function to return a server, if the server is already occupied, try +incrementing the port until a free one is found (after a few tries an error +is thrown). +""" +function get_server( + host, + port, + req_handler; + incr::Int = 0 + ) + + incr >= 10 && @error "couldn't find a free port in $incr tries" try server = HTTP.listen!(host, port; readtimeout=0) do http::HTTP.Stream if HTTP.WebSockets.isupgrade(http.message) @@ -614,7 +687,7 @@ function get_server(host, port, req_handler; incr=0) end end return server, port - catch IOError + catch return get_server(host, port+1, req_handler; incr=incr+1) end end diff --git a/test/dummies/foofile b/test/dummies/foofile new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/test/dummies/foofile @@ -0,0 +1 @@ +foo diff --git a/test/file_watching.jl b/test/file_watching.jl index 212fc97..f8e2df7 100644 --- a/test/file_watching.jl +++ b/test/file_watching.jl @@ -104,14 +104,4 @@ end LS.watch_file!(sw, file3) @test length(sw.watchedfiles) == 3 - - LS.start(sw) - - rm(file3) - sleep(0.25) # needs to be sufficient to give time for propagation. - - # file3 was deleted - @test length(sw.watchedfiles) == 2 - @test sw.watchedfiles[1].path == file1 - @test sw.watchedfiles[2].path == file2 end diff --git a/test/server.jl b/test/server.jl index e99c9fa..ff2768b 100644 --- a/test/server.jl +++ b/test/server.jl @@ -3,15 +3,17 @@ bk = pwd() cd(joinpath(@__DIR__, "..")) req = "tmp" - @test LS.get_fs_path(req) == "" + @test LS.get_fs_path(req) == ("", :not_found_without_404) + req = "/test/dummies/foofile" + @test LS.get_fs_path(req) == ("test/dummies/foofile", :file) req = "/test/dummies/index.html" - @test LS.get_fs_path(req) == "test/dummies/index.html" + @test LS.get_fs_path(req) == ("test/dummies/index.html", :dir_with_index) req = "/test/dummies/r%C3%A9sum%C3%A9/" - @test LS.get_fs_path(req) == "test/dummies/résumé/index.html" + @test LS.get_fs_path(req) == ("test/dummies/résumé/index.html", :dir_with_index) req = "/test/dummies/" - @test LS.get_fs_path(req) == "test/dummies/index.html" + @test LS.get_fs_path(req) == ("test/dummies/index.html", :dir_with_index) req = "/test/dummies/?query=string" - @test LS.get_fs_path(req) == "test/dummies/index.html" + @test LS.get_fs_path(req) == ("test/dummies/index.html", :dir_with_index) cd(bk) end @@ -71,7 +73,7 @@ tasks that you will try to start. # if one asks for something incorrect, a 404 should be returned response = HTTP.get("http://localhost:$port/no.html"; status_exception=false) @test response.status == 404 - @test occursin("404: file not found.", String(response.body)) + @test occursin("404 Not Found", String(response.body)) # test custom 404.html page mkdir("404"); write("404/index.html", "custom 404") response = HTTP.get("http://localhost:$port/no.html"; status_exception=false) @@ -130,8 +132,8 @@ tasks that you will try to start. rm("css", recursive=true) rm("404", recursive=true) sleep(0.5) - # only index.html is still watched - @test length(fw.watchedfiles) == 1 + # everything is still watched (#160) + @test length(fw.watchedfiles) == 4 # # SHUTTING DOWN @@ -176,14 +178,14 @@ end HTTP.Connection(io) ) - fs_path = LS.get_fs_path(s.message.target) + fs_path, _ = LS.get_fs_path(s.message.target) @test fs_path == "test_file.html" tsk = @async LS.HTTP.WebSockets.upgrade(LS.ws_tracker, s) sleep(1.0) # the websocket should have been added to the list @test LS.WS_VIEWERS[fs_path] isa Vector{HTTP.WebSockets.WebSocket} - @test length(LS.WS_VIEWERS[fs_path]) == 1 + @test length(LS.WS_VIEWERS[fs_path]) >= 1 # simulate a "good" closure (an event caused a write on the websocket and then closes it) ws = LS.WS_VIEWERS[fs_path][1]