diff --git a/examples/StippleMakieDemo.jl b/examples/StippleMakieDemo.jl index ab5b755..26d56e0 100644 --- a/examples/StippleMakieDemo.jl +++ b/examples/StippleMakieDemo.jl @@ -1,6 +1,7 @@ using Stipple using Stipple.ReactiveTools using StippleUI +import Genie.Server.openbrowser using StippleMakie @@ -8,14 +9,19 @@ Stipple.enable_model_storage(false) # ------------------------------------------------------------------------------------------------ -# if required set a different port, url or proxy_port for Makie's websocket communication, e.g. -# otherwise, Genie's settings are applied for listen_url and proxy_url and Makie's (Bonito's) settings are applied for the ports -configure_makie_server!(listen_port = 8001) +# if required set a different port, url or proxy_port for Makie's websocket communication, e.g. 8001 +# if not specified, Genie's settings are applied for listen_url and proxy_url and Makie's (Bonito's) settings +# are applied for the ports +# configure_makie_server!(listen_port = 8001) # Example settings for a proxy configuration: -# configure_makie_server!(listen_port = 8001, proxy_url = "/makie", proxy_port = 8080) - +# specify the proxy_port explicitly +configure_makie_server!(listen_port = 8001, proxy_url = "/makie", proxy_port = 8080) +# proxy_port will be taken from the serving port +#configure_makie_server!(listen_port = 8001, proxy_url = "/makie") +startproxy(8080) +# in production settings it might be favorable to use a reverse proxy for the websocket communication, e.g. nginx. # The appropriate nginx configuration can be generated using `nginx_config()` either after setting the configuration # or by passing the desired settings directly to the function. # nginx_config() @@ -44,7 +50,7 @@ UI::ParsedHTMLString = column(style = "height: 80vh; width: 100%", [ cell(col = 4, class = "full-width", makie_figure(:fig1)) h4("MakiePlot 2") cell(col = 5, class = "full-width", makie_figure(:fig2)) - (btn("Hello", @click(:hello), color = "primary")) + btn("Hello", @click(:hello), color = "primary") ]) ui() = UI @@ -58,4 +64,9 @@ route("/") do # page(model, ui, prepend = makie_dom(model)) |> html end -up(open_browser = true) \ No newline at end of file +up() +openbrowser("http://localhost:8080") + +# down() +# close_proxy(8080; force = true) +# close_all_proxies(force = true) \ No newline at end of file diff --git a/src/StippleMakie.jl b/src/StippleMakie.jl index 3227bde..61a63c3 100644 --- a/src/StippleMakie.jl +++ b/src/StippleMakie.jl @@ -1,16 +1,21 @@ module StippleMakie using Stipple +using Stipple.HTTP +using Stipple.HTTP.WebSockets using WGLMakie using WGLMakie.Bonito using WGLMakie.Bonito.URIs using WGLMakie.Bonito.Observables +include("proxy.jl") + # import Stipple.Genie.Router.WS_PROXIES WS_PROXIES = isdefined(Genie.Router, :WS_PROXIES) ? Genie.Router.WS_PROXIES : Dict{String, Any}() export MakieFigure, init_makiefigures, makie_figure, makie_dom, configure_makie_server!, WGLMakie, Makie, nginx_config, once, onready +export startproxy, closeproxy """ once(f::Function, o::Observable) diff --git a/src/proxy.jl b/src/proxy.jl new file mode 100644 index 0000000..2597055 --- /dev/null +++ b/src/proxy.jl @@ -0,0 +1,120 @@ +using Stipple +using Stipple.HTTP.URIs +using Stipple.Genie.Generator.UUIDs + +const BACKEND_HOST = "127.0.0.1" +const SOCKETS = Dict{Int, Dict{UUID, WebSocket}}() +const SERVERS = Dict{Int, HTTP.Server}() + +function proxy_handler(stream::HTTP.Stream; + genie_port = Genie.config.server_port, + genie_ws_port = something(Genie.config.websockets_port, genie_port), + makie_path = URI(Bonito.SERVER_CONFIGURATION.proxy_url[]).path, + makie_port = Bonito.SERVER_CONFIGURATION.listen_port[]) + + startswith(makie_path, "/") || (makie_path = "/$makie_path") + + req = stream.message + host_port = parse(Int, split(Dict(req.headers)["Host"], ":")[2]) + is_makie_path = startswith(req.target, Regex(makie_path * "(/|\$)")) + + is_makie_path && (req.target = req.target[length(makie_path) + 1:end]) + + if WebSockets.isupgrade(req) + backend_port = is_makie_path ? makie_port : genie_ws_port + HTTP.WebSockets.upgrade(stream) do client_ws + client_id = uuid4() + sockets = get!(Dict{UUID, WebSocket}, SOCKETS, host_port) + push!(sockets, client_id => client_ws) + + HTTP.WebSockets.open("http://$BACKEND_HOST:$backend_port$(req.target)") do backend_ws + backend_id = uuid4() + push!(sockets, backend_id => backend_ws) + @async begin + for msg in backend_ws + try + @debug("Sending to client '$(backend_ws.request.target)': $msg") + HTTP.WebSockets.send(client_ws, msg) + catch err + error("Proxy error in backend connection: $err") + close(client_ws) + break + end + end + @debug "Closing backend connection." + close(client_ws) + delete!(sockets, client_id) + end + + for msg in client_ws + try + @debug("Sending to backend '$(client_ws.request.target)': $msg") + HTTP.WebSockets.send(backend_ws, msg) + catch err + error("Proxy error in client connection: $err\nclosing Stream handler") + break + end + end + @debug "Closing client connection." + close(backend_ws) + delete!(sockets, backend_id) + delete!(sockets, client_id) # just to be sure ... + end + end + else + backend_port = is_makie_path ? makie_port : genie_port + backend_url = "http://$BACKEND_HOST:$backend_port$(req.target)" + backend_response = HTTP.request(req.method, backend_url; headers=req.headers, body=req.body) + closeread(stream) + req.body = read(stream) + req.response = backend_response + req.response.request = req + startwrite(stream) + write(stream, req.response.body) + nothing + end +end + +function startproxy(port = 8080; + genie_port = Genie.config.server_port, + genie_ws_port = something(Genie.config.websockets_port, genie_port), + makie_path = URI(Bonito.SERVER_CONFIGURATION.proxy_url[]).path, + makie_port = Bonito.SERVER_CONFIGURATION.listen_port[]) + + function handler(stream::HTTP.Stream) + proxy_handler(stream; genie_port, genie_ws_port, makie_path, makie_port) + end + + server = HTTP.listen!(handler, port) + SERVERS[port] = server +end + +function closeproxy(server; force::Bool = false) + if force + HTTP.Servers.shutdown(server.on_shutdown) + close(server.listener) + Base.@lock server.connections_lock begin + for c in server.connections + HTTP.Servers.requestclose!(c) + end + end + port = parse(Int, server.listener.hostport) + haskey(SOCKETS, port) && [try close(ws) finally end for ws in values(SOCKETS[port])] + delete!(SOCKETS, port) + end + close(server) + return true +end + +function closeproxy(port::Integer; force::Bool = false) + haskey(Servers, port) || return false + closeproxy(SERVERS[port]; force) + delete!(SERVERS, port) + return true +end + +function close_all_proxies(; force::Bool = false) + for port in keys(SERVERS) + closeproxy(port; force) + end +end \ No newline at end of file