secubox-openwrt/package/secubox/secubox-app-streamlit-launcher/files/usr/sbin/streamlit-launcherctl
CyberMind-FR d9bcf1c09b feat(streamlit-launcher): Add on-demand startup with idle shutdown
New package secubox-app-streamlit-launcher:
- Lazy loading: apps start only when accessed
- Idle shutdown: stop apps after configurable timeout (default 30min)
- Memory management: force-stop low-priority apps when memory low
- Priority system: higher priority = keep running longer
- Always-on mode for critical apps
- Procd daemon with respawn

CLI: streamlit-launcherctl
  - daemon: run background manager
  - status/list: show app states and idle times
  - start/stop: manual app control
  - priority: set app priority (1-100)
  - check/check-memory: manual checks

Updated slforge with launcher integration:
- slforge launcher status/priority/always-on commands
- Access tracking on app start
- README documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 07:55:47 +01:00

495 lines
13 KiB
Bash

#!/bin/sh
# Streamlit On-Demand Launcher Controller
# Manages lazy-loading of Streamlit apps with idle shutdown
. /lib/functions.sh
TRACKING_DIR="/tmp/streamlit-access"
STARTUP_DIR="/tmp/streamlit-startup"
LOG_TAG="streamlit-launcher"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m'
# Load config
load_config() {
config_load streamlit-launcher
config_get ENABLED global enabled '1'
config_get IDLE_TIMEOUT global idle_timeout '30'
config_get CHECK_INTERVAL global check_interval '60'
config_get MEMORY_THRESHOLD global memory_threshold '100'
config_get ON_DEMAND global on_demand '1'
config_get STARTUP_TIMEOUT global startup_timeout '30'
config_get TRACKING_DIR global tracking_dir '/tmp/streamlit-access'
config_get LOG_LEVEL global log_level 'info'
}
log() {
local level="$1"
shift
case "$LOG_LEVEL" in
debug) ;;
info) [ "$level" = "debug" ] && return ;;
warn) [ "$level" = "debug" -o "$level" = "info" ] && return ;;
error) [ "$level" != "error" ] && return ;;
esac
logger -t "$LOG_TAG" -p "daemon.$level" "$*"
[ -t 1 ] && echo "[$level] $*"
}
# Get app priority (higher = more important)
get_priority() {
local app="$1"
local priority=50
local always_on=0
get_app_priority() {
local section="$1"
local cfg_app cfg_value cfg_always
config_get cfg_app "$section" app
config_get cfg_value "$section" value '50'
config_get cfg_always "$section" always_on '0'
if [ "$cfg_app" = "$app" ] || [ "$cfg_app" = "*" ]; then
priority="$cfg_value"
always_on="$cfg_always"
fi
}
config_foreach get_app_priority priority
echo "$priority $always_on"
}
# Track app access (called by mitmproxy hook or cron)
track_access() {
local app="$1"
mkdir -p "$TRACKING_DIR"
touch "$TRACKING_DIR/$app"
log debug "Tracked access for $app"
}
# Get last access time (seconds since epoch)
get_last_access() {
local app="$1"
local file="$TRACKING_DIR/$app"
if [ -f "$file" ]; then
stat -c %Y "$file" 2>/dev/null || echo 0
else
echo 0
fi
}
# Get idle time in minutes
get_idle_minutes() {
local app="$1"
local last_access=$(get_last_access "$app")
local now=$(date +%s)
if [ "$last_access" -gt 0 ]; then
echo $(( (now - last_access) / 60 ))
else
echo 9999
fi
}
# Check if app is running
is_running() {
local app="$1"
local port
port=$(uci -q get streamlit-forge."$app".port)
[ -z "$port" ] && return 1
netstat -tln 2>/dev/null | grep -q ":$port " && return 0
return 1
}
# Get free memory in MB
get_free_memory() {
awk '/MemAvailable/{print int($2/1024)}' /proc/meminfo
}
# Start an app (on-demand)
start_app() {
local app="$1"
local wait="${2:-0}"
if is_running "$app"; then
log debug "App $app already running"
track_access "$app"
return 0
fi
log info "Starting app $app on-demand"
mkdir -p "$STARTUP_DIR"
touch "$STARTUP_DIR/$app.starting"
# Start via slforge
slforge start "$app" >/dev/null 2>&1
local rc=$?
if [ "$wait" = "1" ] && [ $rc -eq 0 ]; then
# Wait for app to be ready
local port=$(uci -q get streamlit-forge."$app".port)
local timeout="$STARTUP_TIMEOUT"
local elapsed=0
while [ $elapsed -lt $timeout ]; do
if netstat -tln 2>/dev/null | grep -q ":$port "; then
log info "App $app ready on port $port (${elapsed}s)"
rm -f "$STARTUP_DIR/$app.starting"
track_access "$app"
return 0
fi
sleep 1
elapsed=$((elapsed + 1))
done
log error "App $app failed to start within ${timeout}s"
rm -f "$STARTUP_DIR/$app.starting"
return 1
fi
rm -f "$STARTUP_DIR/$app.starting"
[ $rc -eq 0 ] && track_access "$app"
return $rc
}
# Stop an app
stop_app() {
local app="$1"
local reason="${2:-idle}"
if ! is_running "$app"; then
log debug "App $app not running"
return 0
fi
log info "Stopping app $app (reason: $reason)"
slforge stop "$app" >/dev/null 2>&1
}
# Check idle apps and stop them
check_idle() {
load_config
[ "$ENABLED" = "1" ] || return 0
local app idle priority always_on
# Get list of apps from streamlit-forge
for app in $(uci -q show streamlit-forge 2>/dev/null | grep '=app$' | cut -d. -f2 | cut -d= -f1); do
[ -z "$app" ] && continue
if ! is_running "$app"; then
continue
fi
# Get priority info
read priority always_on <<EOF
$(get_priority "$app")
EOF
# Skip always-on apps
if [ "$always_on" = "1" ]; then
log debug "App $app is always-on, skipping"
continue
fi
idle=$(get_idle_minutes "$app")
log debug "App $app: idle=${idle}m, timeout=${IDLE_TIMEOUT}m, priority=$priority"
if [ "$idle" -ge "$IDLE_TIMEOUT" ]; then
stop_app "$app" "idle ${idle}m > ${IDLE_TIMEOUT}m"
fi
done
}
# Check memory pressure and stop low-priority apps
check_memory() {
load_config
[ "$ENABLED" = "1" ] || return 0
local free_mb=$(get_free_memory)
if [ "$free_mb" -ge "$MEMORY_THRESHOLD" ]; then
return 0
fi
log warn "Low memory: ${free_mb}MB < ${MEMORY_THRESHOLD}MB threshold"
# Build list of running apps with priorities
local apps_by_priority=""
local app priority always_on
for app in $(uci -q show streamlit-forge 2>/dev/null | grep '=app$' | cut -d. -f2 | cut -d= -f1); do
[ -z "$app" ] && continue
is_running "$app" || continue
read priority always_on <<EOF
$(get_priority "$app")
EOF
[ "$always_on" = "1" ] && continue
apps_by_priority="$apps_by_priority
$priority $app"
done
# Sort by priority (lowest first) and stop until memory is OK
echo "$apps_by_priority" | sort -n | while read priority app; do
[ -z "$app" ] && continue
stop_app "$app" "memory_pressure"
sleep 2
free_mb=$(get_free_memory)
if [ "$free_mb" -ge "$MEMORY_THRESHOLD" ]; then
log info "Memory recovered: ${free_mb}MB"
break
fi
done
}
# Request app startup (called by external trigger)
request_start() {
local app="$1"
load_config
if [ "$ON_DEMAND" != "1" ]; then
log debug "On-demand disabled, ignoring start request for $app"
return 1
fi
start_app "$app" 1
}
# Main daemon loop
daemon() {
load_config
log info "Streamlit Launcher daemon starting (idle=${IDLE_TIMEOUT}m, interval=${CHECK_INTERVAL}s)"
mkdir -p "$TRACKING_DIR"
while true; do
check_idle
check_memory
sleep "$CHECK_INTERVAL"
done
}
# Show status
status() {
load_config
echo "Streamlit Launcher Status"
echo "========================="
echo ""
printf "%-20s %s\n" "Enabled:" "$ENABLED"
printf "%-20s %s\n" "On-Demand:" "$ON_DEMAND"
printf "%-20s %sm\n" "Idle Timeout:" "$IDLE_TIMEOUT"
printf "%-20s %ss\n" "Check Interval:" "$CHECK_INTERVAL"
printf "%-20s %sMB\n" "Memory Threshold:" "$MEMORY_THRESHOLD"
printf "%-20s %sMB\n" "Free Memory:" "$(get_free_memory)"
echo ""
echo "Apps:"
echo "-----"
printf "%-20s %-10s %-10s %-10s %-8s\n" "NAME" "STATUS" "IDLE" "PRIORITY" "ALWAYS"
local app priority always_on idle_mins status_str
for app in $(uci -q show streamlit-forge 2>/dev/null | grep '=app$' | cut -d. -f2 | cut -d= -f1); do
[ -z "$app" ] && continue
read priority always_on <<EOF
$(get_priority "$app")
EOF
if is_running "$app"; then
status_str="${GREEN}running${NC}"
idle_mins="$(get_idle_minutes "$app")m"
else
status_str="${RED}stopped${NC}"
idle_mins="-"
fi
[ "$always_on" = "1" ] && always_str="yes" || always_str="no"
printf "%-20s ${status_str}%-10s %-10s %-10s %-8s\n" "$app" "" "$idle_mins" "$priority" "$always_str"
done
}
# List apps with detailed info
list() {
load_config
local app port domain enabled idle_mins status_str priority always_on
printf "%-18s %-8s %-6s %-25s %-8s %-6s\n" "APP" "STATUS" "IDLE" "DOMAIN" "PRIORITY" "ALWAYS"
echo "--------------------------------------------------------------------------------"
for app in $(uci -q show streamlit-forge 2>/dev/null | grep '=app$' | cut -d. -f2 | cut -d= -f1); do
[ -z "$app" ] && continue
port=$(uci -q get streamlit-forge."$app".port)
domain=$(uci -q get streamlit-forge."$app".domain)
enabled=$(uci -q get streamlit-forge."$app".enabled)
read priority always_on <<EOF
$(get_priority "$app")
EOF
if is_running "$app"; then
status_str="running"
idle_mins="$(get_idle_minutes "$app")m"
else
status_str="stopped"
idle_mins="-"
fi
[ "$always_on" = "1" ] && always_str="yes" || always_str="-"
[ -z "$domain" ] && domain="-"
printf "%-18s %-8s %-6s %-25s %-8s %-6s\n" "$app" "$status_str" "$idle_mins" "$domain" "$priority" "$always_str"
done
}
# Set app priority
set_priority() {
local app="$1"
local priority="$2"
local always_on="${3:-0}"
[ -z "$app" ] || [ -z "$priority" ] && {
echo "Usage: $0 priority <app> <priority> [always_on]"
return 1
}
# Check if app exists
uci -q get streamlit-forge."$app" >/dev/null || {
echo "Error: App '$app' not found"
return 1
}
# Find or create priority section
local section=""
find_section() {
local s="$1"
local cfg_app
config_get cfg_app "$s" app
[ "$cfg_app" = "$app" ] && section="$s"
}
config_load streamlit-launcher
config_foreach find_section priority
if [ -z "$section" ]; then
section=$(uci add streamlit-launcher priority)
uci set streamlit-launcher."$section".app="$app"
fi
uci set streamlit-launcher."$section".value="$priority"
uci set streamlit-launcher."$section".always_on="$always_on"
uci commit streamlit-launcher
echo "Set priority for $app: $priority (always_on: $always_on)"
}
# Stop all managed apps
stop_all() {
log info "Stopping all managed apps"
for app in $(uci -q show streamlit-forge 2>/dev/null | grep '=app$' | cut -d. -f2 | cut -d= -f1); do
[ -z "$app" ] && continue
is_running "$app" && stop_app "$app" "stop_all"
done
}
# Start all enabled apps (for non-on-demand mode)
start_all() {
log info "Starting all enabled apps"
for app in $(uci -q show streamlit-forge 2>/dev/null | grep '=app$' | cut -d. -f2 | cut -d= -f1); do
[ -z "$app" ] && continue
local enabled=$(uci -q get streamlit-forge."$app".enabled)
[ "$enabled" = "1" ] && start_app "$app" 0
done
}
usage() {
cat <<EOF
Streamlit On-Demand Launcher
Usage: $0 <command> [options]
COMMANDS:
daemon Run the launcher daemon (called by init.d)
status Show launcher status and app states
list List all apps with details
start <app> Start an app on-demand
stop <app> Stop an app
stop-all Stop all managed apps
start-all Start all enabled apps
track <app> Track access for an app (resets idle timer)
priority <app> <n> Set app priority (higher = keep running longer)
Add '1' as third arg for always-on
check Run idle check once
check-memory Run memory pressure check once
EXAMPLES:
$0 status
$0 start ytdownload
$0 priority control 100 1 # Always keep 'control' running
$0 priority ytdownload 30 # Lower priority, stop sooner
CONFIG: /etc/config/streamlit-launcher
EOF
}
# Main
case "$1" in
daemon)
daemon
;;
status)
status
;;
list)
list
;;
start)
[ -z "$2" ] && { echo "Usage: $0 start <app>"; exit 1; }
load_config
start_app "$2" 1
;;
stop)
[ -z "$2" ] && { echo "Usage: $0 stop <app>"; exit 1; }
load_config
stop_app "$2" "manual"
;;
stop-all)
load_config
stop_all
;;
start-all)
load_config
start_all
;;
track)
[ -z "$2" ] && { echo "Usage: $0 track <app>"; exit 1; }
load_config
track_access "$2"
;;
priority)
set_priority "$2" "$3" "$4"
;;
check)
check_idle
;;
check-memory)
check_memory
;;
request)
[ -z "$2" ] && { echo "Usage: $0 request <app>"; exit 1; }
request_start "$2"
;;
*)
usage
exit 1
;;
esac