#!/bin/sh # SPDX-License-Identifier: Apache-2.0 # LuCI RPC backend for Streamlit Platform # Copyright (C) 2025 CyberMind.fr . /lib/functions.sh . /usr/share/libubox/jshn.sh CONFIG="streamlit" LXC_NAME="streamlit" LXC_PATH="/srv/lxc" APPS_PATH="/srv/streamlit/apps" # JSON helpers json_init_obj() { json_init; json_add_object "result"; } json_close_obj() { json_close_object; json_dump; } json_error() { json_init json_add_object "error" json_add_string "message" "$1" json_close_object json_dump } json_success() { json_init_obj json_add_boolean "success" 1 [ -n "$1" ] && json_add_string "message" "$1" json_close_obj } # Check if container is running lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" } # Check if container exists lxc_exists() { [ -f "$LXC_PATH/$LXC_NAME/config" ] && [ -d "$LXC_PATH/$LXC_NAME/rootfs" ] } # Get service status get_status() { local enabled running installed uptime local http_port data_path memory_limit active_app config_load "$CONFIG" config_get enabled main enabled "0" config_get http_port main http_port "8501" config_get data_path main data_path "/srv/streamlit" config_get memory_limit main memory_limit "512M" config_get active_app main active_app "hello" running="false" installed="false" uptime="" if lxc_exists; then installed="true" fi if lxc_running; then running="true" uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}') fi # Count apps local app_count=0 APPS_PATH="$data_path/apps" if [ -d "$APPS_PATH" ]; then app_count=$(ls -1 "$APPS_PATH"/*.py 2>/dev/null | wc -l) fi # Get LAN IP for URL local lan_ip lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") json_init_obj json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )" json_add_boolean "running" "$( [ "$running" = "true" ] && echo 1 || echo 0 )" json_add_boolean "installed" "$( [ "$installed" = "true" ] && echo 1 || echo 0 )" json_add_string "uptime" "$uptime" json_add_int "http_port" "$http_port" json_add_string "data_path" "$data_path" json_add_string "memory_limit" "$memory_limit" json_add_string "active_app" "$active_app" json_add_int "app_count" "$app_count" json_add_string "web_url" "http://${lan_ip}:${http_port}" json_add_string "container_name" "$LXC_NAME" json_close_obj } # Get configuration get_config() { local http_port http_host data_path memory_limit enabled active_app local headless gather_stats theme_base theme_primary config_load "$CONFIG" # Main settings config_get http_port main http_port "8501" config_get http_host main http_host "0.0.0.0" config_get data_path main data_path "/srv/streamlit" config_get memory_limit main memory_limit "512M" config_get enabled main enabled "0" config_get active_app main active_app "hello" # Server settings config_get headless server headless "true" config_get gather_stats server browser_gather_usage_stats "false" config_get theme_base server theme_base "dark" config_get theme_primary server theme_primary_color "#0ff" json_init_obj json_add_object "main" json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )" json_add_int "http_port" "$http_port" json_add_string "http_host" "$http_host" json_add_string "data_path" "$data_path" json_add_string "memory_limit" "$memory_limit" json_add_string "active_app" "$active_app" json_close_object json_add_object "server" json_add_boolean "headless" "$( [ "$headless" = "true" ] && echo 1 || echo 0 )" json_add_boolean "browser_gather_usage_stats" "$( [ "$gather_stats" = "true" ] && echo 1 || echo 0 )" json_add_string "theme_base" "$theme_base" json_add_string "theme_primary_color" "$theme_primary" json_close_object json_close_obj } # Save configuration save_config() { read -r input local http_port http_host data_path memory_limit enabled active_app local headless gather_stats theme_base theme_primary http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null) http_host=$(echo "$input" | jsonfilter -e '@.http_host' 2>/dev/null) data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null) memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null) enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null) active_app=$(echo "$input" | jsonfilter -e '@.active_app' 2>/dev/null) headless=$(echo "$input" | jsonfilter -e '@.headless' 2>/dev/null) gather_stats=$(echo "$input" | jsonfilter -e '@.browser_gather_usage_stats' 2>/dev/null) theme_base=$(echo "$input" | jsonfilter -e '@.theme_base' 2>/dev/null) theme_primary=$(echo "$input" | jsonfilter -e '@.theme_primary_color' 2>/dev/null) [ -n "$http_port" ] && uci set "${CONFIG}.main.http_port=$http_port" [ -n "$http_host" ] && uci set "${CONFIG}.main.http_host=$http_host" [ -n "$data_path" ] && uci set "${CONFIG}.main.data_path=$data_path" [ -n "$memory_limit" ] && uci set "${CONFIG}.main.memory_limit=$memory_limit" [ -n "$enabled" ] && uci set "${CONFIG}.main.enabled=$enabled" [ -n "$active_app" ] && uci set "${CONFIG}.main.active_app=$active_app" [ -n "$headless" ] && uci set "${CONFIG}.server.headless=$headless" [ -n "$gather_stats" ] && uci set "${CONFIG}.server.browser_gather_usage_stats=$gather_stats" [ -n "$theme_base" ] && uci set "${CONFIG}.server.theme_base=$theme_base" [ -n "$theme_primary" ] && uci set "${CONFIG}.server.theme_primary_color=$theme_primary" uci commit "$CONFIG" json_success "Configuration saved" } # Start service start_service() { if lxc_running; then json_error "Service is already running" return fi if ! lxc_exists; then json_error "Container not installed. Run install first." return fi /etc/init.d/streamlit start >/dev/null 2>&1 & sleep 2 if lxc_running; then json_success "Service started" else json_error "Failed to start service" fi } # Stop service stop_service() { if ! lxc_running; then json_error "Service is not running" return fi /etc/init.d/streamlit stop >/dev/null 2>&1 sleep 2 if ! lxc_running; then json_success "Service stopped" else json_error "Failed to stop service" fi } # Restart service restart_service() { /etc/init.d/streamlit restart >/dev/null 2>&1 & sleep 3 if lxc_running; then json_success "Service restarted" else json_error "Service restart failed" fi } # Install Streamlit install() { if lxc_exists; then json_error "Already installed. Use update to refresh." return fi # Run install in background /usr/sbin/streamlitctl install >/var/log/streamlit-install.log 2>&1 & json_init_obj json_add_boolean "started" 1 json_add_string "message" "Installation started in background" json_add_string "log_file" "/var/log/streamlit-install.log" json_close_obj } # Uninstall Streamlit uninstall() { /usr/sbin/streamlitctl uninstall >/dev/null 2>&1 if ! lxc_exists; then json_success "Uninstalled successfully" else json_error "Uninstall failed" fi } # Update Streamlit update() { if ! lxc_exists; then json_error "Not installed. Run install first." return fi # Run update in background /usr/sbin/streamlitctl update >/var/log/streamlit-update.log 2>&1 & json_init_obj json_add_boolean "started" 1 json_add_string "message" "Update started in background" json_add_string "log_file" "/var/log/streamlit-update.log" json_close_obj } # Get logs get_logs() { read -r input local lines lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null) [ -z "$lines" ] && lines=100 local data_path config_load "$CONFIG" config_get data_path main data_path "/srv/streamlit" json_init_obj json_add_array "logs" # Get container logs from data path if [ -d "$data_path/logs" ]; then local logfile for logfile in "$data_path/logs"/*.log; do [ -f "$logfile" ] || continue tail -n "$lines" "$logfile" 2>/dev/null | while IFS= read -r line; do json_add_string "" "$line" done done fi # Also check install/update logs for logfile in /var/log/streamlit-install.log /var/log/streamlit-update.log; do [ -f "$logfile" ] || continue tail -n 50 "$logfile" 2>/dev/null | while IFS= read -r line; do json_add_string "" "$line" done done json_close_array json_close_obj } # List apps list_apps() { local data_path active_app config_load "$CONFIG" config_get data_path main data_path "/srv/streamlit" config_get active_app main active_app "hello" APPS_PATH="$data_path/apps" json_init_obj json_add_array "apps" if [ -d "$APPS_PATH" ]; then for app in "$APPS_PATH"/*.py; do [ -f "$app" ] || continue local name=$(basename "$app" .py) local size=$(ls -la "$app" 2>/dev/null | awk '{print $5}') local mtime=$(stat -c %Y "$app" 2>/dev/null || echo "0") local is_active=0 [ "$name" = "$active_app" ] && is_active=1 json_add_object "" json_add_string "name" "$name" json_add_string "path" "$app" json_add_string "size" "$size" json_add_int "mtime" "$mtime" json_add_boolean "active" "$is_active" json_close_object done fi json_close_array json_add_string "active_app" "$active_app" json_add_string "apps_path" "$APPS_PATH" json_close_obj } # Add app add_app() { read -r input local name path name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null) if [ -z "$name" ] || [ -z "$path" ]; then json_error "Missing name or path" return fi /usr/sbin/streamlitctl app add "$name" "$path" >/dev/null 2>&1 if [ $? -eq 0 ]; then json_success "App added: $name" else json_error "Failed to add app" fi } # Remove app remove_app() { read -r input local name name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) if [ -z "$name" ]; then json_error "Missing app name" return fi /usr/sbin/streamlitctl app remove "$name" >/dev/null 2>&1 if [ $? -eq 0 ]; then json_success "App removed: $name" else json_error "Failed to remove app" fi } # Set active app set_active_app() { read -r input local name name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) if [ -z "$name" ]; then json_error "Missing app name" return fi /usr/sbin/streamlitctl app run "$name" >/dev/null 2>&1 if [ $? -eq 0 ]; then json_success "Active app set to: $name" else json_error "Failed to set active app" fi } # Get app details get_app() { read -r input local name name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) if [ -z "$name" ]; then json_error "Missing app name" return fi local data_path active_app config_load "$CONFIG" config_get data_path main data_path "/srv/streamlit" config_get active_app main active_app "hello" local app_file="$data_path/apps/${name}.py" if [ ! -f "$app_file" ]; then json_error "App not found" return fi local size=$(ls -la "$app_file" 2>/dev/null | awk '{print $5}') local mtime=$(stat -c %Y "$app_file" 2>/dev/null || echo "0") local lines=$(wc -l < "$app_file" 2>/dev/null || echo "0") local is_active=0 [ "$name" = "$active_app" ] && is_active=1 json_init_obj json_add_string "name" "$name" json_add_string "path" "$app_file" json_add_string "size" "$size" json_add_int "mtime" "$mtime" json_add_int "lines" "$lines" json_add_boolean "active" "$is_active" json_close_obj } # Upload app (receive base64 content) upload_app() { read -r input local name content name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) content=$(echo "$input" | jsonfilter -e '@.content' 2>/dev/null) if [ -z "$name" ] || [ -z "$content" ]; then json_error "Missing name or content" return fi local data_path config_load "$CONFIG" config_get data_path main data_path "/srv/streamlit" local app_file="$data_path/apps/${name}.py" mkdir -p "$data_path/apps" # Decode base64 and write echo "$content" | base64 -d > "$app_file" 2>/dev/null if [ $? -eq 0 ]; then # Register in UCI uci set "${CONFIG}.${name}=app" uci set "${CONFIG}.${name}.name=$name" uci set "${CONFIG}.${name}.path=${name}.py" uci set "${CONFIG}.${name}.enabled=1" uci commit "$CONFIG" json_success "App uploaded: $name" else json_error "Failed to decode app content" fi } # List instances list_instances() { json_init_obj json_add_array "instances" config_load "$CONFIG" _add_instance_json() { local section="$1" local name app port enabled autostart inst_name config_get inst_name "$section" name "" config_get app "$section" app "" config_get port "$section" port "" config_get enabled "$section" enabled "0" config_get autostart "$section" autostart "0" [ -z "$app" ] && return json_add_object "" json_add_string "id" "$section" json_add_string "name" "$inst_name" json_add_string "app" "$app" json_add_int "port" "$port" json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )" json_add_boolean "autostart" "$( [ "$autostart" = "1" ] && echo 1 || echo 0 )" json_close_object } config_foreach _add_instance_json instance json_close_array json_close_obj } # Add instance add_instance() { read -r input local id name app port id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) app=$(echo "$input" | jsonfilter -e '@.app' 2>/dev/null) port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null) if [ -z "$id" ] || [ -z "$app" ] || [ -z "$port" ]; then json_error "Missing id, app, or port" return fi [ -z "$name" ] && name="$id" # Validate port number if ! echo "$port" | grep -qE '^[0-9]+$'; then json_error "Invalid port number" return fi # Check if instance already exists local existing existing=$(uci -q get "${CONFIG}.${id}") if [ -n "$existing" ]; then json_error "Instance $id already exists" return fi uci set "${CONFIG}.${id}=instance" uci set "${CONFIG}.${id}.name=$name" uci set "${CONFIG}.${id}.app=$app" uci set "${CONFIG}.${id}.port=$port" uci set "${CONFIG}.${id}.enabled=1" uci set "${CONFIG}.${id}.autostart=1" uci commit "$CONFIG" json_success "Instance added: $id" } # Remove instance remove_instance() { read -r input local id id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) if [ -z "$id" ]; then json_error "Missing instance id" return fi # Check if instance exists local existing existing=$(uci -q get "${CONFIG}.${id}") if [ -z "$existing" ]; then json_error "Instance $id not found" return fi uci delete "${CONFIG}.${id}" uci commit "$CONFIG" json_success "Instance removed: $id" } # Enable instance enable_instance() { read -r input local id id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) if [ -z "$id" ]; then json_error "Missing instance id" return fi uci set "${CONFIG}.${id}.enabled=1" uci commit "$CONFIG" json_success "Instance enabled: $id" } # Disable instance disable_instance() { read -r input local id id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) if [ -z "$id" ]; then json_error "Missing instance id" return fi uci set "${CONFIG}.${id}.enabled=0" uci commit "$CONFIG" json_success "Instance disabled: $id" } # Check install progress get_install_progress() { local log_file="/var/log/streamlit-install.log" local status="unknown" local progress=0 local message="" if [ -f "$log_file" ]; then # Check for completion markers if grep -q "Installation complete" "$log_file" 2>/dev/null; then status="completed" progress=100 message="Installation completed successfully" elif grep -q "ERROR" "$log_file" 2>/dev/null; then status="error" message=$(grep "ERROR" "$log_file" | tail -1) else status="running" # Estimate progress based on log content if grep -q "Rootfs created" "$log_file" 2>/dev/null; then progress=80 message="Setting up container..." elif grep -q "Extracting rootfs" "$log_file" 2>/dev/null; then progress=60 message="Extracting container rootfs..." elif grep -q "Downloading Alpine" "$log_file" 2>/dev/null; then progress=40 message="Downloading Alpine rootfs..." elif grep -q "Installing Streamlit" "$log_file" 2>/dev/null; then progress=20 message="Starting installation..." else progress=10 message="Initializing..." fi fi else status="not_started" message="Installation has not been started" fi # Check if process is still running if pgrep -f "streamlitctl install" >/dev/null 2>&1; then status="running" fi json_init_obj json_add_string "status" "$status" json_add_int "progress" "$progress" json_add_string "message" "$message" json_close_obj } # Main RPC handler case "$1" in list) cat <<-EOF { "get_status": {}, "get_config": {}, "save_config": {"http_port": 8501, "http_host": "str", "data_path": "str", "memory_limit": "str", "enabled": "str", "active_app": "str", "headless": "str", "browser_gather_usage_stats": "str", "theme_base": "str", "theme_primary_color": "str"}, "start": {}, "stop": {}, "restart": {}, "install": {}, "uninstall": {}, "update": {}, "get_logs": {"lines": 100}, "list_apps": {}, "get_app": {"name": "str"}, "add_app": {"name": "str", "path": "str"}, "remove_app": {"name": "str"}, "set_active_app": {"name": "str"}, "upload_app": {"name": "str", "content": "str"}, "get_install_progress": {}, "list_instances": {}, "add_instance": {"id": "str", "name": "str", "app": "str", "port": 8501}, "remove_instance": {"id": "str"}, "enable_instance": {"id": "str"}, "disable_instance": {"id": "str"} } EOF ;; call) case "$2" in get_status) get_status ;; get_config) get_config ;; save_config) save_config ;; start) start_service ;; stop) stop_service ;; restart) restart_service ;; install) install ;; uninstall) uninstall ;; update) update ;; get_logs) get_logs ;; list_apps) list_apps ;; get_app) get_app ;; add_app) add_app ;; remove_app) remove_app ;; set_active_app) set_active_app ;; upload_app) upload_app ;; get_install_progress) get_install_progress ;; list_instances) list_instances ;; add_instance) add_instance ;; remove_instance) remove_instance ;; enable_instance) enable_instance ;; disable_instance) disable_instance ;; *) json_error "Unknown method: $2" ;; esac ;; esac