diff --git a/back/Containerfile.larvatagger b/back/Containerfile.larvatagger index 48fd0592ce9fd716eabcb25b45e4b586ce49a813..bc9e4afd5715d7fce6187cc49fe3bc5070761454 100644 --- a/back/Containerfile.larvatagger +++ b/back/Containerfile.larvatagger @@ -52,9 +52,6 @@ RUN cd "${JULIA_PROJECT}" \ # final stage -ARG LARVATAGGER_PORT=9286 -ENV LARVATAGGER_PORT=${LARVATAGGER_PORT} - COPY --chown=julia larvatagger-entrypoint.sh /app/ ENTRYPOINT ["/app/larvatagger-entrypoint.sh"] diff --git a/back/larvatagger-entrypoint.sh b/back/larvatagger-entrypoint.sh index 70a99f84c5bab0e925e0be9b56b6dbeca364515a..aa3148262e7ecbd5645582d987dbc231d0ad3486 100755 --- a/back/larvatagger-entrypoint.sh +++ b/back/larvatagger-entrypoint.sh @@ -2,6 +2,14 @@ if [ -z "$LARVATAGGER_PORT" ]; then LARVATAGGER_PORT=9286 +else + echo "Using environment variable: LARVATAGGER_PORT= $LARVATAGGER_PORT" fi -julia -e "using LarvaTagger.REST.Server; run_backend(\"/app\"; host=\"0.0.0.0\", port=$LARVATAGGER_PORT)" +if [ -z "$LARVATAGGER_TOKEN_EXPIRY" ]; then + LARVATAGGER_TOKEN_EXPIRY=14400 +else + echo "Using environment variable: LARVATAGGER_TOKEN_EXPIRY= $LARVATAGGER_TOKEN_EXPIRY" +fi + +julia -e "using LarvaTagger.REST.Server; run_backend(\"/app\", $LARVATAGGER_TOKEN_EXPIRY; host=\"0.0.0.0\", port=$LARVATAGGER_PORT)" diff --git a/back/larvatagger-no-build.sh b/back/larvatagger-no-build.sh index f505281ad3213e379d7f7d1c6d564d019f39f286..7101dd78a518d69cc5900982cd093c28b29184bc 100755 --- a/back/larvatagger-no-build.sh +++ b/back/larvatagger-no-build.sh @@ -4,13 +4,22 @@ if [ -z "$LARVATAGGER_PORT" ]; then LARVATAGGER_PORT=9286 +else + echo "Using environment variable: LARVATAGGER_PORT= $LARVATAGGER_PORT" fi if [ -z "$LARVATAGGER_IMAGE" ]; then LARVATAGGER_IMAGE=docker.io/flaur/larvatagger:0.20-bigfat +else + echo "Using environment variable: LARVATAGGER_IMAGE= $LARVATAGGER_IMAGE" +fi +if [ -z "$LARVATAGGER_TOKEN_EXPIRY" ]; then + LARVATAGGER_TOKEN_EXPIRY=3600 +else + echo "Using environment variable: LARVATAGGER_TOKEN_EXPIRY= $LARVATAGGER_TOKEN_EXPIRY" fi -podman run -d -p $LARVATAGGER_PORT:9285 --entrypoint=julia $LARVATAGGER_IMAGE \ - --project=/app -e 'using LarvaTagger.REST.Server; run_backend("/app"; host="0.0.0.0")' +podman run -d -p $LARVATAGGER_PORT:9285 --entrypoint=julia $LARVATAGGER_IMAGE --project=/app \ + -e "using LarvaTagger.REST.Server; run_backend(\"/app\", $LARVATAGGER_TOKEN_EXPIRY; host=\"0.0.0.0\")" CONTAINER=`podman ps | grep $LARVATAGGER_IMAGE | cut -d' ' -f1` if ! [ -z "$CONTAINER" ]; then diff --git a/src/GenieExtras.jl b/src/GenieExtras.jl index 40cf48a4fa308ea6ab9a3b9c735bfdc133852c19..ebe416c469fe35307a2cbe5faa8386b83008bf6e 100644 --- a/src/GenieExtras.jl +++ b/src/GenieExtras.jl @@ -1,8 +1,11 @@ module GenieExtras import Genie.Renderer.Html +import GenieSession: GenieSession, Session +using NyxWidgets.Base: Cache -export publish_css, appinfo, add_footer +export publish_css, appinfo, add_footer, Session, SessionRegistry, getsessionid, getsession, + clearexpired const project_root = dirname(Base.current_project()) @@ -49,4 +52,129 @@ add_footer(ui::AbstractString, footer=footer) = "$ui$(footer())" add_footer(ui::Function, footer=footer) = () -> add_footer(ui(), footer) +struct SessionRegistry + register + idlength + purgesession + purgesession_args +end + +function SessionRegistry(f, args=(:session_id,); idlength=32) + SessionRegistry(Cache{String, Dict{Symbol, Any}}(), idlength, f, args) +end + +Base.lock(f::Function, registry::SessionRegistry) = lock(f, registry.register) + +function check_session_id_length(registry, session_id) + if !isnothing(registry.idlength) && length(session_id) < registry.idlength + throw("wrong session_id length") + end +end + +function Base.haskey(registry::SessionRegistry, session_id::AbstractString) + lock(registry) do + for session_long_id in keys(registry.register.cache) + if startswith(session_long_id, session_id) + return true + end + end + return false + end +end + +function Base.getindex(registry::SessionRegistry, session_id::AbstractString) + check_session_id_length(registry, session_id) + lock(registry) do + for (session_long_id, records) in pairs(registry.register.cache) + if startswith(session_long_id, session_id) + records[:lastseen] = time() + return records + end + end + throw(KeyError(session_id)) + end +end + +Base.getindex(registry::SessionRegistry, session_id::AbstractString, record::Symbol + ) = registry[session_id][record] + +function getsessionid(session_long_id::String, idlength::Union{Nothing, <:Integer}) + session_id = session_long_id + if !isnothing(idlength) + session_id = string(session_id[1:idlength]) + end + return session_id +end + +function getsessionid(registry::SessionRegistry) + session = GenieSession.session() + session_long_id = session.id + session_id = getsessionid(session_long_id, registry.idlength) + # create the entry in the register if missing + records = lock(registry) do + register = registry.register.cache + if session_id != session_long_id && session_id in keys(register) + # if the session has been registered with its short id (and `getsession`), + # update the short id with the long one + register[session_long_id] = pop!(register, session_id) + else + # registry.register is a NyxWidgets.Cache and `getindex` behaves like `get!` + registry.register[session_long_id] + end + end + # register minimum information + records[:session] = session + records[:lastseen] = time() + # run maintenance routine + clearexpired(registry) + return session_id +end + +function getsession(registry::SessionRegistry, session_id::AbstractString) + if haskey(registry, session_id) # check for long id + registry[session_id] + else # call get! on short id (if session_id is short); not ideal + registry.register[session_id] + end +end + +function clearexpired(registry, expiry::Real=604800) + lock(registry) do + register = registry.register.cache + for (session_long_id, records) in pairs(register) + if !haskey(records, :lastseen) + @warn "Missing key: lastseen" + continue + end + expiry <= time() - records[:lastseen] || continue + # prepare to clear the session; collect the arguments to purgesession + session_id = getsessionid(session_long_id, registry.idlength) + available_args = Dict(:session_id=>session_id, + :session_long_id=>session_long_id, + records...) + @info "Purging session" session_id + pop!(register, session_long_id) + if haskey(records, :session) + session = records[:session] + # see learn.genieframework.com/framework/genie.jl/recipes/session + empty!(session.data) + else + @warn "Missing key: session" + end + # + function get′(arg) + get(available_args, arg) do + @warn "Missing argument for purgesession" arg + nothing + end + end + try + registry.purgesession(get′.(registry.purgesession_args)...) + catch + @error "\`purgesession\` callback failed" + end + end + end +end + end diff --git a/src/apps/larvatagger/app.jl b/src/apps/larvatagger/app.jl index b02e1aa44276703051166a5173ef992c17936306..7b3081623789d4afc10123143615d6dcce0ebebe 100644 --- a/src/apps/larvatagger/app.jl +++ b/src/apps/larvatagger/app.jl @@ -3,7 +3,7 @@ module LarvaTagger using NyxUI, NyxUI.Storage, NyxUI.GenieExtras using NyxWidgets.Base: Cache import LarvaTagger as LT -using GenieSession, Stipple +using Stipple import Stipple: @app, @init, @private, @in, @onchange, @onbutton, @click import StippleUI: tooltip @@ -19,24 +19,30 @@ mutable struct Model kwargs end -const bonito_models = Cache{String, Model}() +function purgesession(model, session_id) + if isnothing(model) + @warn "Session data prematurely lost" + elseif !isnothing(model.app) + close(model.app) + end + purge_appdata(session_id, "larvatagger") + BonitoServer.addsession(bonito_app, session_id; restart=true) +end + +const session_registry = SessionRegistry(purgesession, (:model, :session_id)) const bonito_app = NamedApp("larvatagger", function (session) bucket = getbucket(session, "larvatagger", :read) mkpath(bucket) - model = lock(bonito_models) do - if haskey(bonito_models, session) - bonito_models.cache[session] - else - kwargs = Dict{Symbol, Any}() - if haskey(ENV, "LARVATAGGER_BACKEND") - kwargs[:backend_directory] = backend = ENV["LARVATAGGER_BACKEND"] - @info "Using environment variable" LARVATAGGER_BACKEND=backend - end - inputfile = Ref{Union{Nothing, String}}(nothing) - bonito_models.cache[session] = Model(1.0, nothing, inputfile, kwargs) + model = get!(getsession(session_registry, session), :model) do + kwargs = Dict{Symbol, Any}() + if !isempty(get(ENV, "LARVATAGGER_BACKEND", "")) + kwargs[:backend_directory] = backend = ENV["LARVATAGGER_BACKEND"] + @info "Using environment variable" LARVATAGGER_BACKEND=backend end + inputfile = Ref{Union{Nothing, String}}(nothing) + Model(1.0, nothing, inputfile, kwargs) end exportdir = joinpath("public", session, "larvatagger") function prepare_download(srcfile) @@ -66,13 +72,9 @@ const bonito_app = NamedApp("larvatagger", end ) -function purgesession(session) - model = lock(bonito_models) do - pop!(bonito_models.cache, session) - end - isnothing(model.app) || close(model.app) - purge_appdata(session, "larvatagger") - BonitoServer.addsession(bonito_app, session; restart=true) +function purgesession(session_id) + model = pop!(session_registry[session_id], :model, nothing) + purgesession(model, session_id) end @@ -82,10 +84,10 @@ end @in reset_bonito_session = false @onchange isready begin - genie_session = GenieSession.session() - bonito_session_id = genie_session.id[1:32] - if haskey(bonito_models, bonito_session_id) - sizefactor = sizefactors_str[bonito_models[bonito_session_id].sizefactor] + bonito_session_id = getsessionid(session_registry) + if haskey(session_registry[bonito_session_id], :model) + model = session_registry[bonito_session_id, :model] + sizefactor = sizefactors_str[model.sizefactor] else BonitoServer.addsession(bonito_app, bonito_session_id) end @@ -103,9 +105,10 @@ end @onchange sizefactor begin new_sizefactor = sizefactors[sizefactor] - if new_sizefactor != bonito_models[bonito_session_id].sizefactor + model = session_registry[bonito_session_id, :model] + if new_sizefactor != model.sizefactor @info "New size factor; refreshing the iframe" - bonito_models[bonito_session_id].sizefactor = new_sizefactor + model.sizefactor = new_sizefactor BonitoServer.addsession(bonito_app, bonito_session_id; restart=true) # restart width = appwidth[sizefactor] height = appheight[sizefactor] @@ -119,7 +122,11 @@ end end @onbutton reset_bonito_session begin - purgesession(bonito_session_id) + try + purgesession(bonito_session_id) + catch + @error "Error while resetting session" + end # reset UI to defaults bonito_session_id[!] = "" sizefactor[!] = "1.0" diff --git a/src/apps/muscles/Backbone.jl b/src/apps/muscles/Backbone.jl index 5547f559f2007fab4c3a50d8d513536747340842..e833bebe2ec0837a427baf7011d7bfe586d3b8d8 100644 --- a/src/apps/muscles/Backbone.jl +++ b/src/apps/muscles/Backbone.jl @@ -3,6 +3,7 @@ module Backbone using NyxWidgets.Base: Cache using NyxUI.MuscleActivities using NyxUI.Storage +using NyxUI.GenieExtras using Bonito include("MuscleWidgets.jl") @@ -32,37 +33,35 @@ export hasmodel, getmodel, setmodel, newsequence, getsequence, withsequences, ## models -const __backbone_cache__ = Cache{String, MuscleWidget}() -const __valid_sessions__ = Dict{String, Bool}() +function purgesession end + +const session_registry = SessionRegistry(purgesession, (:session_id, )) function app(persistent=""; title="Nyx muscle activity") App(; title=title) do session - # # embedded apps are always renderded twice; save time # EDIT: except on Ctrl+R - # valid = get(__valid_sessions__, persistent, false) - # __valid_sessions__[persistent] = !valid - # valid || return @info "New session" session.id cache_id = isempty(persistent) ? session.id : persistent - model = __backbone_cache__[cache_id] + model = session_registry[cache_id, :model] Bonito.jsrender(session, model) end end ## muscle model -getmodel(session_id) = __backbone_cache__[session_id]#.widget - -# for debugging -getmodel() = first(values(__backbone_cache__))#.widget +getmodel(session_id) = session_registry[session_id, :model] -hasmodel(session_id) = haskey(__backbone_cache__, session_id) +hasmodel(session_id) = haskey(session_registry[session_id], :model) function setmodel(session_id, sequence) - __backbone_cache__[session_id] = MuscleWidget(sequence) + records = getsession(session_registry, session_id) + records[:model] = model = MuscleWidget(sequence) + # init :sequences + get!(records, :sequences) do + Cache{Int, MuscleActivity}() + end + return model end -# getchannel(session_id) = __backbone_cache__[session_id].channel - ## sequence model function newsequencename(stem, existing) @@ -97,14 +96,13 @@ function newsequence(session_id, sequence_name, sequence_names, start_time, time return sequence end -const __sequences__ = Cache{String, Cache{Int, MuscleActivity}}() - function getsequence(session_id, sequence_name) model = getmodel(session_id) sequence = nothing if model.sequence[].program_name != sequence_name - lock(__sequences__[session_id]) do - for outer sequence in values(__sequences__[session_id]) + sequences = session_registry[session_id, :sequences] + lock(sequences) do + for outer sequence in values(sequences) sequence.program_name == sequence_name && break end end @@ -116,16 +114,16 @@ end getsequence(session_id) = getmodel(session_id).sequence[] function addsequence(session_id, sequence) - lock(__sequences__[session_id]) do - sequences = __sequences__[session_id] + sequences = session_registry[session_id, :sequences] + lock(sequences) do i = isempty(sequences) ? 0 : maximum(keys(sequences)) sequences[i+1] = sequence end end function withsequences(f, session_id) - lock(__sequences__[session_id]) do - sequences = __sequences__[session_id] + sequences = session_registry[session_id, :sequences] + lock(sequences) do return f(sequences) end end @@ -153,7 +151,7 @@ end function deletesequence(session_id) model = getmodel(session_id) current = model.sequence[].program_name - sequences = __sequences__[session_id] + sequences = session_registry[session_id, :sequences] lock(sequences) do for (ix, seq) in pairs(sequences) if seq.program_name == current @@ -167,19 +165,7 @@ function deletesequence(session_id) end function purgesession(session_id; appname="muscles") - model = lock(__backbone_cache__) do - if session_id in keys(__backbone_cache__.cache) - #pop!(__valid_sessions__, session_id) # not sure what to do with it - pop!(__backbone_cache__.cache, session_id) - end - end - lock(__sequences__) do - if session_id in keys(__sequences__.cache) - pop!(__sequences__.cache, session_id) - end - end - purge_appdata(session_id, appname) - return model + purge_appdata(session_id, appname) end end diff --git a/src/apps/muscles/app.jl b/src/apps/muscles/app.jl index 6140bd2cb844c63fc6efdb74953b6a4ca7958b12..044e4cccdfc0cdf4fe2c0c067892be1daf998909 100644 --- a/src/apps/muscles/app.jl +++ b/src/apps/muscles/app.jl @@ -2,7 +2,7 @@ module MuscleApp using NyxUI, NyxUI.Storage, NyxUI.GenieExtras using Observables -using Genie, GenieSession, Stipple, StippleUI +using Genie, Stipple, StippleUI import Stipple: @app, @init, @private, @in, @out, @onchange, @onbutton, @notify include("Backbone.jl") @@ -20,6 +20,8 @@ const maxlength = getconfig("muscle-activity", "maxlength") const bonito_app = NamedApp(:inherit, Backbone.app) +Backbone.purgesession(session_id) = purgesession(session_id; appname=bonito_app.name) + @app begin @private bonito_session_id = "" @@ -95,7 +97,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app) end @onbutton reset_bonito_session begin - model = purgesession(bonito_session_id; appname=bonito_app.name) + purgesession(bonito_session_id) # reset UI to defaults #bonito_session_id[!] = "" sequence[!] = MuscleActivity("") @@ -445,8 +447,7 @@ function init_model(muscle_model=nothing) muscle_model = @init #@show muscle_model end - genie_session = GenieSession.session() - session_id = genie_session.id[1:32] + session_id = getsessionid(Backbone.session_registry) if hasmodel(session_id) withsequences(session_id) do sequences ids = sort!(collect(keys(sequences)))