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>
This commit is contained in:
CyberMind-FR 2026-03-14 07:55:47 +01:00
parent fb0584f44b
commit d9bcf1c09b
8 changed files with 1004 additions and 0 deletions

View File

@ -52,6 +52,11 @@ slforge hide <app> # Remove public access
slforge publish <app> # Add to mesh catalog
slforge unpublish <app> # Remove from mesh
# Launcher Integration (on-demand startup)
slforge launcher status # Show launcher status
slforge launcher priority <app> <n> # Set app priority (1-100)
slforge launcher always-on <app> # Mark as always-on
# Templates
slforge templates # List available templates
```
@ -182,3 +187,44 @@ Published apps create a manifest at `/usr/share/secubox/plugins/catalog/`:
}
}
```
## On-Demand Launcher
Install `secubox-app-streamlit-launcher` for resource optimization:
- **Lazy Loading** - Apps start only when first accessed
- **Idle Shutdown** - Stop apps after configurable timeout (default: 30 min)
- **Memory Management** - Force-stop low-priority apps when memory is low
- **Priority System** - Keep critical apps running longer
### Launcher Commands
```bash
# Check launcher status
slforge launcher status
# Set app priority (higher = keep running longer, max 100)
slforge launcher priority myapp 75
# Mark app as always-on (never auto-stopped)
slforge launcher always-on dashboard
```
### Priority Levels
| Priority | Behavior |
|----------|----------|
| 100 + always_on | Never auto-stopped |
| 80-99 | Stopped last during memory pressure |
| 50 (default) | Normal priority |
| 1-49 | Stopped first during memory pressure |
### How It Works
1. User accesses `https://app.example.com`
2. If app is stopped, launcher starts it on-demand
3. App access time is tracked
4. After idle timeout, app is automatically stopped
5. Memory pressure triggers low-priority app shutdown
See `secubox-app-streamlit-launcher` README for full configuration.

View File

@ -32,6 +32,24 @@ log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_err() { echo -e "${RED}[ERROR]${NC} $1"; }
# Launcher integration
LAUNCHER_TRACKING_DIR="/tmp/streamlit-access"
# Track app access (for launcher idle detection)
track_access() {
local name="$1"
mkdir -p "$LAUNCHER_TRACKING_DIR"
touch "$LAUNCHER_TRACKING_DIR/$name"
}
# Check if launcher is managing this app
is_launcher_managed() {
local enabled
config_load streamlit-launcher 2>/dev/null || return 1
config_get enabled global enabled '0'
[ "$enabled" = "1" ]
}
usage() {
cat <<EOF
Streamlit Forge - App Publishing Platform
@ -77,6 +95,11 @@ Mesh AppStore:
catalog Browse mesh catalog
install <app@node> Install from mesh
Launcher Integration:
launcher status Show launcher status
launcher priority <app> <n> Set app priority (higher=keep longer)
launcher always-on <app> Mark app as always-on (never auto-stop)
Templates:
templates List available templates
@ -664,6 +687,8 @@ STARTSCRIPT
if netstat -tln 2>/dev/null | grep -q ":$port "; then
[ "$quiet" = "0" ] && log_ok "Started $name"
[ "$quiet" = "0" ] && log_info "URL: http://192.168.255.1:$port"
# Track access for launcher idle detection
track_access "$name"
return 0
fi
@ -940,6 +965,56 @@ cmd_unpublish() {
log_ok "Unpublished $name from mesh catalog"
}
# Launcher integration commands
cmd_launcher() {
local action="$1"
shift
case "$action" in
status)
if command -v streamlit-launcherctl >/dev/null 2>&1; then
streamlit-launcherctl status
else
log_warn "Launcher not installed"
log_info "Install with: opkg install secubox-app-streamlit-launcher"
fi
;;
priority)
local app="$1"
local priority="$2"
[ -z "$app" ] || [ -z "$priority" ] && {
log_err "Usage: slforge launcher priority <app> <priority>"
return 1
}
if command -v streamlit-launcherctl >/dev/null 2>&1; then
streamlit-launcherctl priority "$app" "$priority"
else
log_err "Launcher not installed"
return 1
fi
;;
always-on)
local app="$1"
[ -z "$app" ] && {
log_err "Usage: slforge launcher always-on <app>"
return 1
}
if command -v streamlit-launcherctl >/dev/null 2>&1; then
streamlit-launcherctl priority "$app" 100 1
log_ok "App $app marked as always-on"
else
log_err "Launcher not installed"
return 1
fi
;;
*)
log_err "Unknown launcher action: $action"
echo "Usage: slforge launcher <status|priority|always-on>"
return 1
;;
esac
}
# Main command router
case "$1" in
create) shift; cmd_create "$@" ;;
@ -961,6 +1036,7 @@ case "$1" in
publish) shift; cmd_publish "$@" ;;
unpublish) shift; cmd_unpublish "$@" ;;
templates) cmd_templates ;;
launcher) shift; cmd_launcher "$@" ;;
help|--help|-h|"")
usage ;;
*)

View File

@ -0,0 +1,48 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-streamlit-launcher
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=SecuBox Team
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-streamlit-launcher
SECTION:=secubox
CATEGORY:=SecuBox
TITLE:=Streamlit On-Demand Launcher
DEPENDS:=+secubox-app-streamlit-forge
PKGARCH:=all
endef
define Package/secubox-app-streamlit-launcher/description
On-demand Streamlit app launcher with idle shutdown.
Starts apps only when accessed, stops after idle timeout.
Monitors memory and can force-stop low-priority apps.
endef
define Build/Compile
endef
define Package/secubox-app-streamlit-launcher/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/streamlit-launcher $(1)/etc/config/
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/streamlit-launcher $(1)/etc/init.d/
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/streamlit-launcherctl $(1)/usr/sbin/
$(INSTALL_DIR) $(1)/usr/share/streamlit-launcher
$(INSTALL_DATA) ./files/usr/share/streamlit-launcher/loading.html $(1)/usr/share/streamlit-launcher/
endef
define Package/secubox-app-streamlit-launcher/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || /etc/init.d/streamlit-launcher enable
exit 0
endef
$(eval $(call BuildPackage,secubox-app-streamlit-launcher))

View File

@ -0,0 +1,191 @@
# SecuBox Streamlit Launcher
On-demand Streamlit app launcher with idle shutdown and memory management.
## Overview
The Streamlit Launcher optimizes resource usage on constrained devices by:
- **Starting apps on-demand** when first accessed (lazy loading)
- **Stopping idle apps** after configurable timeout (default: 30 min)
- **Managing memory pressure** by stopping low-priority apps when memory is low
- **Priority system** to keep critical apps running longer
## Architecture
```
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ HAProxy │────▶│ mitmproxy │────▶│ Streamlit │
│ (vhost) │ │ (WAF+track) │ │ Launcher Daemon │
└─────────────┘ └──────────────┘ └────────┬────────┘
│ │
Track access Start/Stop
│ │
┌──────▼──────┐ ┌─────▼─────┐
│ /tmp/access │ │ slforge │
│ (touch) │ │ start/ │
└─────────────┘ │ stop │
└───────────┘
```
## Installation
```bash
opkg install secubox-app-streamlit-launcher
```
## CLI Reference
```bash
# Show status
streamlit-launcherctl status
# List all apps with details
streamlit-launcherctl list
# Manually start/stop an app
streamlit-launcherctl start <app>
streamlit-launcherctl stop <app>
# Set app priority (higher = keep running longer)
streamlit-launcherctl priority <app> <value>
# Set always-on (never auto-stop)
streamlit-launcherctl priority <app> 100 1
# Run idle check manually
streamlit-launcherctl check
# Run memory pressure check
streamlit-launcherctl check-memory
```
## Configuration
Edit `/etc/config/streamlit-launcher`:
```
config global 'global'
# Enable the launcher daemon
option enabled '1'
# Enable on-demand startup (vs always-running)
option on_demand '1'
# Minutes of inactivity before stopping an app
option idle_timeout '30'
# Seconds between idle checks
option check_interval '60'
# Minimum free memory (MB) before force-stopping apps
option memory_threshold '100'
# Max seconds to wait for app startup
option startup_timeout '30'
# App priorities (higher = keep running longer)
config priority 'control'
option app 'control'
option value '100'
option always_on '1'
config priority 'ytdownload'
option app 'ytdownload'
option value '30'
```
## Priority System
| Priority | Behavior |
|----------|----------|
| 100 + always_on | Never auto-stopped |
| 80-99 | Stopped last during memory pressure |
| 50 (default) | Normal priority |
| 1-49 | Stopped first during memory pressure |
## Integration with slforge
The launcher works alongside `slforge` (Streamlit Forge):
- `slforge` manages app configuration, creation, and basic start/stop
- `streamlit-launcherctl` adds on-demand and idle management
When on-demand is enabled:
1. User accesses `https://app.example.com`
2. HAProxy routes to mitmproxy
3. If app is down, mitmproxy can trigger startup via hook
4. Launcher starts app and waits for ready
5. Request is served
6. Access is tracked
7. After idle timeout, app is stopped
## Access Tracking
The launcher tracks app access via touch files in `/tmp/streamlit-access/`:
```bash
# Track access (reset idle timer)
streamlit-launcherctl track <app>
# Or directly
touch /tmp/streamlit-access/<app>
```
This can be triggered by:
- mitmproxy request hook
- HAProxy health check script
- Cron job parsing access logs
## Memory Management
When free memory drops below threshold:
1. Apps are sorted by priority (lowest first)
2. Low-priority apps are stopped one by one
3. Stops when memory recovers above threshold
4. Always-on apps are never stopped
## Service Control
```bash
# Enable/start the daemon
/etc/init.d/streamlit-launcher enable
/etc/init.d/streamlit-launcher start
# Check daemon status
/etc/init.d/streamlit-launcher status
# View logs
logread -e streamlit-launcher
```
## Files
| Path | Description |
|------|-------------|
| `/usr/sbin/streamlit-launcherctl` | CLI tool |
| `/etc/config/streamlit-launcher` | UCI configuration |
| `/etc/init.d/streamlit-launcher` | Procd init script |
| `/tmp/streamlit-access/` | Access tracking files |
| `/usr/share/streamlit-launcher/loading.html` | Loading page template |
## Example: Optimize for Low Memory
```bash
# Set aggressive timeout (10 min)
uci set streamlit-launcher.global.idle_timeout='10'
# Lower memory threshold (trigger cleanup at 150MB free)
uci set streamlit-launcher.global.memory_threshold='150'
# Make dashboard always-on
streamlit-launcherctl priority dashboard 100 1
# Lower priority for heavy apps
streamlit-launcherctl priority jupyter 20
streamlit-launcherctl priority analytics 30
uci commit streamlit-launcher
/etc/init.d/streamlit-launcher restart
```

View File

@ -0,0 +1,28 @@
config global 'global'
option enabled '1'
# Idle timeout in minutes before stopping an app
option idle_timeout '30'
# Check interval in seconds
option check_interval '60'
# Minimum free memory (MB) before force-stopping low-priority apps
option memory_threshold '100'
# Enable on-demand startup (vs always-running)
option on_demand '1'
# Startup timeout in seconds (max wait for app to be ready)
option startup_timeout '30'
# Directory for access tracking files
option tracking_dir '/tmp/streamlit-access'
# Log level: debug, info, warn, error
option log_level 'info'
# App priority (higher = keep running longer)
# Apps not listed default to priority 50
config priority 'default'
option app '*'
option value '50'
# Example: keep control dashboard always running
#config priority 'control'
# option app 'control'
# option value '100'
# option always_on '1'

View File

@ -0,0 +1,31 @@
#!/bin/sh /etc/rc.common
START=95
STOP=10
USE_PROCD=1
PROG=/usr/sbin/streamlit-launcherctl
start_service() {
local enabled
config_load streamlit-launcher
config_get enabled global enabled '0'
[ "$enabled" = "1" ] || return 0
procd_open_instance
procd_set_param command "$PROG" daemon
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "streamlit-launcher"
}
reload_service() {
stop
start
}

View File

@ -0,0 +1,494 @@
#!/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

View File

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="3">
<title>Starting App...</title>
<style>
:root {
--bg: #0a0e17;
--card: #161e2e;
--text: #e2e8f0;
--muted: #94a3b8;
--green: #00C853;
--blue: #2979FF;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', system-ui, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
padding: 40px;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid var(--card);
border-top-color: var(--green);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 24px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
}
p {
color: var(--muted);
font-size: 14px;
margin-bottom: 8px;
}
.hint {
font-size: 12px;
color: var(--muted);
opacity: 0.7;
margin-top: 24px;
}
.progress {
width: 200px;
height: 4px;
background: var(--card);
border-radius: 2px;
margin: 20px auto;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--green), var(--blue));
animation: progress 3s ease-in-out infinite;
}
@keyframes progress {
0% { width: 0%; }
50% { width: 70%; }
100% { width: 100%; }
}
</style>
</head>
<body>
<div class="container">
<div class="spinner"></div>
<h1>Starting Application</h1>
<p>The app is being launched on-demand...</p>
<div class="progress">
<div class="progress-bar"></div>
</div>
<p class="hint">This page will refresh automatically</p>
</div>
</body>
</html>