#!/usr/bin/env bash set -euo pipefail # Default config for: # curl -fsSL https://punchsky.cn/install_iot.sh | bash # # Fill these values before publishing this script at https://punchsky.cn/install_iot.sh. WORK_DIR="${WORK_DIR:-/opt/app}" CURRENT_USER="${SUDO_USER:-$(id -un)}" SERVICE_USER="${SERVICE_USER:-$CURRENT_USER}" SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn "$SERVICE_USER")}" # Configure programs here before publishing this script. # Format: # INSTALL_=( # "service_name" # "download_url" # "start_command" # "stop_command" # "service_user" # "service_group" # ) # # IOT_ENV is rendered into the systemd service file as Environment= lines, # using the [prod] section from iot/env.file. Directory creation is driven by # those service env values. IOT_DIR_ENV_KEYS lists env keys whose values are # directories that must be created if they do not exist. IOT_PARENT_DIR_ENV_KEYS # lists env keys whose values are file paths; their parent directories must be # created if they do not exist. # # Use {install_dir}, {work_dir} and {service_name} in commands and directories. # service_user and service_group are optional. INSTALL_IOT=( "iot" "https://punchsky.cn/down/iot-0.1.0.tar.gz" "{install_dir}/bin/iot foreground" "{install_dir}/bin/iot stop" "" "" ) IOT_ENV=( "IOT_API_URL=http://127.0.0.1/api/v1" "ENDPOINT_ROOT_DIR=/var/lib/endpoint/database/" "ENDPOINT_LOG_PATH=/var/lib/endpoint/endpoint_log" "IOT_MNESIA_DIR=/var/lib/iot/mnesia/" "IOT_CTRL_SOCKET_PATH=/var/lib/iot/ctl.sock" ) IOT_DIR_ENV_KEYS=( "ENDPOINT_ROOT_DIR" "ENDPOINT_LOG_PATH" "IOT_MNESIA_DIR" ) IOT_PARENT_DIR_ENV_KEYS=( "IOT_CTRL_SOCKET_PATH" ) TEMP_FILES=() INSTALLED_SERVICES=() usage() { cat <<'EOF' Usage: curl -fsSL https://punchsky.cn/install_iot.sh | bash curl -fsSL https://punchsky.cn/install_iot.sh | bash -s -- \ -w Config: Configure programs inside this script before publishing it. Options: -w, --work-dir Working directory used for installation. --user Default service user. Default: current user. --group Default service group. Default: current user's primary group. -h, --help Show this help. Array config inside script: INSTALL_IOT IOT_ENV IOT_DIR_ENV_KEYS IOT_PARENT_DIR_ENV_KEYS Package format: INSTALL_=( "service_name" "download_url" "start_command" "stop_command" "service_user" "service_group" ) Placeholders: {install_dir} will be replaced with the extracted package directory. {work_dir} will be replaced with the root work directory. {service_name} will be replaced with the service name. Example: curl -fsSL https://punchsky.cn/install_iot.sh | bash EOF } log() { printf '[install] %s\n' "$*" } cleanup() { local file set +u for file in "${TEMP_FILES[@]}"; do [[ -n "$file" ]] && rm -f "$file" done set -u } fail() { printf '[install][error] %s\n' "$*" >&2 exit 1 } require_command() { command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1" } require_option_value() { local option="$1" local value="${2:-}" [[ -n "$value" ]] || fail "$option requires a value" } require_root_runner() { if [[ "${EUID}" -ne 0 ]]; then require_command sudo fi } run_as_root() { if [[ "${EUID}" -eq 0 ]]; then "$@" else sudo "$@" fi } strip_archive_suffix() { local filename="$1" case "$filename" in *.tar.gz) printf '%s\n' "${filename%.tar.gz}" ;; *.tgz) printf '%s\n' "${filename%.tgz}" ;; *) fail "Unsupported archive suffix: $filename. Expected .tar.gz or .tgz" ;; esac } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -w|--work-dir) require_option_value "$1" "${2:-}" WORK_DIR="${2:-}" shift 2 ;; --user) require_option_value "$1" "${2:-}" SERVICE_USER="${2:-}" shift 2 ;; --group) require_option_value "$1" "${2:-}" SERVICE_GROUP="${2:-}" shift 2 ;; -h|--help) usage exit 0 ;; *) fail "Unknown argument: $1" ;; esac done } validate_args() { [[ -n "$WORK_DIR" ]] || fail "--work-dir is required" [[ "$WORK_DIR" = /* ]] || fail "--work-dir must be an absolute path" require_install_config INSTALL_IOT } prepare_work_dir() { log "Creating root work directory: $WORK_DIR" run_as_root install -d -m 0755 "$WORK_DIR" } render_service_file() { local service_name="$1" local service_user="$2" local service_group="$3" local install_dir="$4" local start_command="$5" local stop_command="$6" local env_line cat </dev/null } validate_install_item() { local service_name="$1" local download_url="$2" local start_command="$3" [[ -n "$service_name" ]] || fail "Invalid package config: service name is empty" [[ -n "$download_url" ]] || fail "Invalid package config for $service_name: download URL is empty" [[ -n "$start_command" ]] || fail "Invalid package config for $service_name: start command is empty" [[ "$service_name" =~ ^[a-zA-Z0-9_.@-]+$ ]] || fail "Invalid service name: $service_name" } require_install_config() { local config_name="$1" local config_count local service_name local download_url local start_command eval "config_count=\${#${config_name}[@]}" eval "service_name=\"\${${config_name}[0]:-}\"" eval "download_url=\"\${${config_name}[1]:-}\"" eval "start_command=\"\${${config_name}[2]:-}\"" [[ "$config_count" -ge 4 ]] || fail "Invalid $config_name config. Expected service name, URL and commands" [[ -n "$service_name" ]] || fail "Invalid $config_name config: service name is empty" [[ -n "$download_url" ]] || fail "Invalid $config_name config: download URL is empty" [[ -n "$start_command" ]] || fail "Invalid $config_name config: start command is empty" } replace_placeholders() { local value="$1" local install_dir="$2" local service_name="$3" value="${value//\{install_dir\}/$install_dir}" value="${value//\{work_dir\}/$WORK_DIR}" value="${value//\{service_name\}/$service_name}" printf '%s\n' "$value" } validate_environment_line() { local env_line="$1" local env_key="${env_line%%=*}" [[ "$env_line" == *=* ]] || fail "Invalid environment line: $env_line" [[ "$env_key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || fail "Invalid environment key: $env_key" } service_env_lines() { local service_name="$1" local env_array_name="" local env_count local index local env_line case "$service_name" in iot) env_array_name="IOT_ENV" ;; *) return 0 ;; esac eval "env_count=\${#${env_array_name}[@]}" for ((index=0; index