Skip to content

Commit

Permalink
bringing in http1 changes (#146)
Browse files Browse the repository at this point in the history
* bringing in http1 changes (v2)
  • Loading branch information
tlienart authored Aug 23, 2022
1 parent 1bc0dc1 commit ded962e
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 129 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
fail-fast: false
matrix:
version:
- '1.3'
- '1.6'
- '1'
os:
- ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "LiveServer"
uuid = "16fef848-5104-11e9-1b77-fb7a48bbb589"
authors = ["Jonas Asprion <jonas.asprion@gmx.ch", "Thibaut Lienart <tlienart@me.com>"]
version = "0.9.2"
version = "1.0.0"

[deps]
Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
Expand All @@ -14,6 +14,6 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
Crayons = "4"
HTTP = "0.8, 0.9"
HTTP = "1"
MIMEs = "0.1"
julia = "1.3"
julia = "1.6"
12 changes: 0 additions & 12 deletions src/LiveServer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,6 @@ const WS_VIEWERS = Dict{String,Vector{HTTP.WebSockets.WebSocket}}()
"""Keep track of whether an interruption happened while processing a websocket."""
const WS_INTERRUPT = Base.Ref{Bool}(false)

# to fix lots of false error messages from HTTP
# https://github.com/JuliaWeb/HTTP.jl/pull/546
# we do HTTP.Stream{HTTP.Messages.Request,S} instead of just HTTP.Stream to prevent the Julia warning about incremental compilation
# This hack was kindly suggested by Fons van der Plas, the author of Pluto.jl see
# https://github.com/fonsp/Pluto.jl/commit/34d41e63138ee6dad178cd9916d4721441eaf710
function HTTP.closebody(http::HTTP.Stream{HTTP.Messages.Request,S}) where S <: IO
http.writechunked || return
http.writechunked = false
try; write(http.stream, "0\r\n\r\n"); catch; end
return
end

#
# Functions
#
Expand Down
4 changes: 3 additions & 1 deletion src/client.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- browser-reload script, automatically added by the LiveServer.jl -->
<script>
var ws_liveserver_M3sp9 = new WebSocket((location.protocol=="https:" ? "wss:" : "ws:") + "//" + location.host + location.pathname);
var ws_liveserver_M3sp9 = new WebSocket(
(location.protocol=="https:" ? "wss:" : "ws:") + "//" + location.host + location.pathname
);
ws_liveserver_M3sp9.onmessage = function(msg) {
if (msg.data === "update") {
ws_liveserver_M3sp9.close();
Expand Down
6 changes: 3 additions & 3 deletions src/file_watching.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ has not changed, and 1 if it has changed.
function has_changed(wf::WatchedFile)
if !isfile(wf.path)
# isfile may return false for a file
# currently being written. Wait for 0.1s
# currently being written. Wait for 0.1s
# then retry once more:
sleep(0.1)
isfile(wf.path) || return -1
Expand Down Expand Up @@ -111,8 +111,8 @@ function file_watcher_task!(fw::FileWatcher)
catch err
fw.status = :interrupted
# an InterruptException is the normal way for this task to end
if !isa(err, InterruptException)
error("An error happened whilst watching files; shutting down. Error was: $err")
if !isa(err, InterruptException) && VERBOSE[]
@error "fw error" exception=(err, catch_backtrace())
end
return nothing
end
Expand Down
135 changes: 76 additions & 59 deletions src/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,31 @@ 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})
foreach(wss) do wsi
try
write(wsi, "update")
close(wsi)
catch
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
try
send(wsi, "update")
catch
end
end
end
empty!(wss)

# force close all viewers (these will be replaced by 'fresh' ones
# after the reload triggered by the update message)
@sync for wsi in ws_to_update_and_close
isopen(wsi.io) && @async begin
try
wsi.writeclosed = wsi.readclosed = true
close(wsi.io)
catch
end
end
end

return nothing
end

Expand Down Expand Up @@ -331,41 +348,21 @@ function serve_file(
return resp
end

"""
ws_upgrade(http::HTTP.Stream)
Upgrade the HTTP request in the stream to a websocket.
"""
function ws_upgrade(http::HTTP.Stream)
# adapted from HTTP.WebSockets.upgrade; note that here the upgrade will always
# have the right format as it always triggered by after a Response
HTTP.setstatus(http, 101)
HTTP.setheader(http, "Upgrade" => "websocket")
HTTP.setheader(http, "Connection" => "Upgrade")
key = HTTP.header(http, "Sec-WebSocket-Key")
HTTP.setheader(http, "Sec-WebSocket-Accept" => HTTP.WebSockets.accept_hash(key))
HTTP.startwrite(http)

io = http.stream
return HTTP.WebSockets.WebSocket(io; server=true)
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.
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, target::AbstractString)
# add to list of html files being "watched"
# NOTE: this file always exists because the query is generated just after
# serving it
fs_path = get_fs_path(target)

# if the file is already being viewed, add ws to it (e.g. several tabs)
# otherwise add to dict
if fs_path keys(WS_VIEWERS)
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)

# 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]
Expand Down Expand Up @@ -463,7 +460,7 @@ directory. (See also [`example`](@ref) for an example folder).
start(fw)

# make request handler
req_handler = HTTP.RequestHandlerFunction() do req
req_handler = HTTP.Handlers.streamhandler() do req
req = preprocess_request(req)
serve_file(
fw, req;
Expand All @@ -472,31 +469,19 @@ directory. (See also [`example`](@ref) for an example folder).
)
end

server = Sockets.listen(parse(IPAddr, host), port)
url = "http://$(host == string(Sockets.localhost) ? "localhost" : host):$port"
server, port = get_server(host, port, req_handler)
host_str = ifelse(host == string(Sockets.localhost), "localhost", host)
url = "http://$host_str:$port"
println(
"✓ LiveServer listening on $url/ ...\n (use CTRL+C to shut down)"
)
@async HTTP.listen(host, port;
server=server, readtimeout=0, reuse_limit=0) do http::HTTP.Stream
# reuse_limit=0 ensures that there won't be an error if killing and restarting the server.
if HTTP.WebSockets.is_upgrade(http.message)
# upgrade to websocket
ws = ws_upgrade(http)
# add to list of viewers and keep open until written to
ws_tracker(ws, http.message.target)
else
# handle HTTP request
HTTP.handle(req_handler, http)
end
end

launch_browser && open_in_default_browser(url)
# wait until user interrupts the LiveServer (using CTRL+C).
try
counter = 1
while true
if WS_INTERRUPT.x || fw.status == :interrupted
if WS_INTERRUPT[] || fw.status == :interrupted
# rethrow the interruption (which may have happened during
# the websocket handling or during the file watching)
throw(InterruptException())
Expand All @@ -509,25 +494,57 @@ directory. (See also [`example`](@ref) for an example folder).
end
catch err
if !isa(err, InterruptException)
if VERBOSE[]
@error "serve error" exception=(err, catch_backtrace())
end
throw(err)
end
finally
# cleanup: close everything that might still be alive
VERBOSE[] && println("\n⋮ shutting down LiveServer")
print("\n⋮ shutting down LiveServer")
# stop the filewatcher
stop(fw)
# close any remaining websockets
for wss values(WS_VIEWERS), wsi wss
close(wsi.io)
for wss values(WS_VIEWERS)
@sync for wsi in wss
isopen(wsi.io) && @async begin
try
wsi.writeclosed = wsi.readclosed = true
close(wsi.io)
catch
end
end
end
end
# empty the dictionary of viewers
empty!(WS_VIEWERS)
# shut down the server
close(server)
VERBOSE[] && println("\n✓ LiveServer shut down.")
HTTP.Servers.forceclose(server)
# reset environment variables
CONTENT_DIR[] = ""
CONTENT_DIR[] = ""
WS_INTERRUPT[] = false
println("")
end
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
try
server = HTTP.listen!(host, port; readtimeout=0) do http::HTTP.Stream
if HTTP.WebSockets.isupgrade(http.message)
# upgrade to websocket and add to list of viewers and keep open
# until written to
HTTP.WebSockets.upgrade(ws_tracker, http)
else
# handle HTTP request
return req_handler(http)
end
end
return server, port
catch IOError
return get_server(host, port+1, req_handler; incr=incr+1)
end
end
2 changes: 1 addition & 1 deletion src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ end
Set the verbosity of LiveServer to either `true` (showing messages upon events) or `false` (default).
"""
setverbose(b::Bool) = (VERBOSE.x = b)
setverbose(b::Bool) = (VERBOSE[] = b)


"""
Expand Down
Loading

2 comments on commit ded962e

@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/66892

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.0.0 -m "<description of version>" ded962e8d20445d3b6847a38ceed1ccaf69f3ed9
git push origin v1.0.0

Please sign in to comment.