Skip to content

Commit

Permalink
wait for figures before writing to them, enhanced proxy capabilities,…
Browse files Browse the repository at this point in the history
… nginx_config()
  • Loading branch information
hhaensel committed Dec 8, 2024
1 parent bcefdbf commit b43e506
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 69 deletions.
96 changes: 82 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,56 @@
StippleMakie is a plugin for the GenieFramework to enable Makie plots via WGLMakie


WGLMakie needs its own websocket port to communicate with the plots. Therefore operation behind a proxy needs a second available port,
which can be configured by `configure_makie_server!`. In the future we might integrate automatic port forwarding with the Genie settings, but that's still work in progress.

### Demo App
Don't be surprised if the first loading time of the Webpage is very long (about a minute).
```
using Stipple, Stipple.ReactiveTools
using StippleMakie
```julia
using Stipple
using Stipple.ReactiveTools
using StippleUI

using WGLMakie
using StippleMakie

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)

configure_makie_server!()
# Example settings for a proxy configuration:
# configure_makie_server!(listen_port = 8001, proxy_url = "/makie", proxy_port = 8080)


# 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()

@app MakieDemo begin
@out fig1 = MakieFigure()
@out fig2 = MakieFigure()
@in hello = false

@onbutton hello @notify "Hello World!"

@onchange isready begin
init_makiefigures(__model__)
sleep(0.3)
Makie.scatter(fig1.fig[1, 1], (0:4).^3)
Makie.heatmap(fig2.fig[1, 1], rand(5, 5))
Makie.scatter(fig2.fig[1, 2], (0:4).^3)
# the viewport changes when the figure is ready to be written to
onready(fig1) do
Makie.scatter(fig1.fig[1, 1], (0:4).^3)
Makie.heatmap(fig2.fig[1, 1], rand(5, 5))
Makie.scatter(fig2.fig[1, 2], (0:4).^3)
end
end
end


UI::ParsedHTMLString = column(style = "height: 80vh; width: 98vw", [
h4("MakiePlot 1")
cell(col = 4, class = "full-width", makie_figure(:fig1))
h4("MakiePlot 2")
cell(col = 4, class = "full-width", makie_figure(:fig2))
btn("Hello", @click(:hello), color = "primary")
])

ui() = UI
Expand All @@ -53,4 +68,57 @@ end

up(open_browser = true)
```
![Form](docs/demoapp.png)
![Form](docs/demoapp.png)

As WGLMakie needs its own websocket port to communicate with the plots, operation behind a proxy needs more careful proxy setup.
After setting up the server, e.g. with `configure_makie_server!(listen_port = 8001, proxy_base_path = "/makie")`, `nginx_conf()` returns a valid
configuration for an nginx server to accomodate running Genie and Makie over the same port.
Here's the nginx configuration for above configuration.

```
http {
upstream makie {
server localhost:8001;
}
upstream genie {
server localhost:8000;
}
server {
listen 8080;
# Proxy traffic to /makie/* to http://localhost:8001/*
location /makie {
proxy_pass http://makie/;
# WebSocket headers
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Preserve headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy all other traffic to http://localhost:8000/*
location / {
proxy_pass http://genie/;
# WebSocket headers
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Preserve headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
```
23 changes: 18 additions & 5 deletions examples/StippleMakieDemo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,29 @@ Stipple.enable_model_storage(false)
# 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)

# Example settings for a proxy configuration:
# configure_makie_server!(listen_port = 8001, proxy_url = "/makie", proxy_port = 8080)


# 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()

@app MakieDemo begin
@out fig1 = MakieFigure()
@out fig2 = MakieFigure()
@in hello = false

@onbutton hello @notify "Hello World!"

@onchange isready begin
init_makiefigures(__model__)
# Wait until plots are ready to be written to
sleep(0.3)
Makie.scatter(fig1.fig[1, 1], (0:4).^3)
Makie.heatmap(fig2.fig[1, 1], rand(5, 5))
Makie.scatter(fig2.fig[1, 2], (0:4).^3)
# the viewport changes when the figure is ready to be written to
onready(fig1) do
Makie.scatter(fig1.fig[1, 1], (0:4).^3)
Makie.heatmap(fig2.fig[1, 1], rand(5, 5))
Makie.scatter(fig2.fig[1, 2], (0:4).^3)
end
end
end

Expand All @@ -32,6 +44,7 @@ UI::ParsedHTMLString = column(style = "height: 80vh; width: 98vw", [
cell(col = 4, class = "full-width", makie_figure(:fig1))
h4("MakiePlot 2")
cell(col = 4, class = "full-width", makie_figure(:fig2))
btn("Hello", @click(:hello), color = "primary")
])

ui() = UI
Expand Down
173 changes: 123 additions & 50 deletions src/StippleMakie.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,54 @@ using Stipple

using WGLMakie
using WGLMakie.Bonito
using Stipple.HTTP
using WGLMakie.Bonito.URIs
using WGLMakie.Bonito.Observables

# 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
export MakieFigure, init_makiefigures, makie_figure, makie_dom, configure_makie_server!, WGLMakie, Makie, nginx_config, once, onready

"""
once(f::Function, o::Observable)
Runs a function once when the observable changes the first time.
# Example
```julia
o = Observable(1)
once(o) do
println("I only say this once!")
end
```
"""
function once(f::Function, o::Observable)
ref = Ref{ObserverFunction}()
ref[] = on(o) do o
f(o)
off(ref[])
end
end

Base.@kwdef mutable struct MakieFigure
fig::Figure = Figure()
session::Union{Nothing, Bonito.Session} = nothing
id = -1
end

"""
onready(f::Function, mf::MakieFigure)
Runs a function once when the viewport of the figure is ready.
# Example
```julia
onready(fig1) do
Makie.scatter(fig1.fig[1, 1], (0:4).^3)
end
"""
onready(f::Function, mf::MakieFigure) = once(_ -> f(), mf.fig.scene.viewport)

Stipple.render(mf::MakieFigure) = Dict(
:js_id1 => "$(mf.id)",
:js_id2 => "$(mf.id + 1)"
Expand Down Expand Up @@ -67,58 +102,96 @@ function Base.empty!(mf::Union{MakieFigure, R{MakieFigure}})
trim!(mf.fig.layout)
end

function configure_makie_server!(; listen_host = nothing, listen_port = Bonito.SERVER_CONFIGURATION.listen_port[], proxy_host = nothing, proxy_port = nothing)
listen_url = something(listen_host, Genie.config.server_host)
proxy_host = something(proxy_host, Genie.config.websockets_exposed_host, Genie.config.websockets_host)
proxy_url = proxy_port === nothing ? nothing : join(filter(!isempty, strip.(["http://$proxy_host:$proxy_port", Genie.config.base_path, Genie.config.websockets_base_path], '/')), "/")
"""
configure_makie_server!(; listen_host = nothing, listen_port = Bonito.SERVER_CONFIGURATION.listen_port[], proxy_url = nothing, proxy_port = nothing)
Configures the Makie server with the specified settings. The default values are taken from Makie's and Genie server configuration.
Parameters:
- `listen_host`: The host to listen on, defaults to `Genie.config.websockets_host`, e.g. `0.0.0.0` or `127.0.0.1`
- `listen_port`: The port to listen on, e.g. `8001`
- `proxy_url`: The URL to proxy traffic to, `'/makie'` or `'http:localhost:8080/_makie_'`
- `proxy_port`: The port to proxy traffic to, e.g. `8080`, this setting overrides port settings in `proxy_url`
"""
function configure_makie_server!(; listen_host = nothing, listen_port = nothing, proxy_url = nothing, proxy_port = nothing)
listen_url = something(listen_host, Genie.config.websockets_host, Genie.config.server_host)
listen_port = something(listen_port, Bonito.SERVER_CONFIGURATION.listen_port[])
proxy_url = something(proxy_url, Genie.config.websockets_exposed_host, "")
isempty(proxy_url) || startswith(proxy_url, "http") || startswith(proxy_url, "/") || (proxy_url = "http://$proxy_url")
uri = URI(something(proxy_url, ""))
host, port, path, scheme = uri.host, uri.port, uri.path, uri.scheme
# let the proxy port override the port
port = something(proxy_port, port)

# if port is defined but host is not, set host to localhost
if !isempty(port) && isempty(host)
host = "127.0.0.1"
scheme = "http"
end

uri = isempty(port) ? URI(; host, path, scheme) : URI(; host, port, path, scheme)
proxy_url = "$uri"
Bonito.configure_server!(; listen_url, listen_port, proxy_url)
(; listen_url, listen_port, proxy_url)
end

"""
nginx_config(; genie_port = nothing, makie_port = Bonito.SERVER_CONFIGURATION.listen_port[], makie_proxy_path = nothing)
# in the future we're trying to internally redirect Makie's websocket traffic to Genie's ws server, WIP
Generates an nginx configuration for proxying traffic to Makie and Genie servers.
# WS_PROXIES["makie_proxy"] = "ws://localhost:$listen_port/dummy"
# Bonito.configure_server!(listen_url = "localhost", listen_port = listen_port, proxy_url = "http://localhost:$proxy_port/makie_proxy")

# routename = join(filter(!isempty, [Genie.config.base_path, Genie.config.websockets_base_path, "makie_proxy/assets/:bonito"]), "/")
# println("routename: $routename")
# route("/$routename") do
# asset = params(:bonito)
# @debug "loading asset via proxy: $asset"
# res = HTTP.get("http://localhost:$listen_port/assets/$asset")

# if endswith(asset, "-Websocket.bundled.js")
# # modify the Makie websocket client to suppress control messages from Genie's ws server
# res.body = replace(String(res.body), r"( *)const binary = new Uint8Array\(evt.data\);" =>
# s"""
# \1const binary = new Uint8Array(evt.data);
# \1
# \1if (typeof(evt.data) == 'string') {
# \1 return resolve(null);
# \1}
# """, "send_pings();" => "") |> Vector{UInt8}
# end

# return res
# end

# Genie.Router.channel("/", named = :makie) do
# @debug begin
# client = Genie.Requests.wsclient()
# """ws proxy in: from $(client.request.target) to
# ... $(WS_PROXIES["makie_proxy"].request.url)"
# """
# end
# msg = Genie.Requests.payload(:raw)
# @debug "ws proxy <-: $(String(deepcopy(msg)))"
# Base.@lock Genie.Router.wslock try
# sleep(0.1)
# HTTP.WebSockets.send(WS_PROXIES["makie_proxy"], msg)
# catch e
# @error "ws proxy <-: $(e)"
# end
# end

return nothing
If not specified otherwise, the configuration takes into account the current configuration of the Genie and Makie servers.
"""
function nginx_config(proxy_port = 8080; genie_port = nothing, makie_port = Bonito.SERVER_CONFIGURATION.listen_port[], makie_proxy_path = nothing)
genie_port = something(genie_port, Genie.config.websockets_port, Genie.config.server_port)
makie_proxy_path = lstrip(something(makie_proxy_path, isempty(Bonito.SERVER_CONFIGURATION.proxy_url[]) ? "_makie_" : Bonito.SERVER_CONFIGURATION.proxy_url[]), '/')
"""
http {
upstream makie {
server localhost:$makie_port;
}
upstream genie {
server localhost:$genie_port;
}
server {
listen $proxy_port;
# Proxy traffic to /$makie_proxy_path/* to http://localhost:$makie_port;/*
location /$makie_proxy_path/ {
proxy_pass http://makie/;
# WebSocket headers
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
# Preserve headers
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
# Proxy all other traffic to http://localhost:$genie_port/*
location / {
proxy_pass http://genie/;
# WebSocket headers
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
# Preserve headers
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
}
"""
end

function __init__()
Expand Down

0 comments on commit b43e506

Please sign in to comment.