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:
parent
fb0584f44b
commit
d9bcf1c09b
@ -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.
|
||||
|
||||
@ -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 ;;
|
||||
*)
|
||||
|
||||
48
package/secubox/secubox-app-streamlit-launcher/Makefile
Normal file
48
package/secubox/secubox-app-streamlit-launcher/Makefile
Normal 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))
|
||||
191
package/secubox/secubox-app-streamlit-launcher/README.md
Normal file
191
package/secubox/secubox-app-streamlit-launcher/README.md
Normal 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
|
||||
```
|
||||
@ -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'
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user