mirror of
https://github.com/openhop-dev/openhop_repeater.git
synced 2026-10-24 05:52:59 +00:00
2003 lines
83 KiB
Bash
Executable File
2003 lines
83 KiB
Bash
Executable File
#!/bin/bash
|
||
# openHop Repeater Management Script - Deploy, Upgrade, Uninstall
|
||
|
||
set -Eeuo pipefail
|
||
|
||
readonly SCRIPT_PATH="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)/$(basename -- "${BASH_SOURCE[0]}")"
|
||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||
|
||
readonly REPOSITORY_URL="https://github.com/openhop-dev/openhop_repeater.git"
|
||
readonly DEFAULT_UPGRADE_REF="dev"
|
||
readonly PACKAGE_DIST_NAME="openhop_repeater"
|
||
readonly LOCK_FILE="/run/lock/openhop-repeater-manage.lock"
|
||
|
||
readonly INSTALL_DIR="/opt/openhop_repeater"
|
||
readonly VENV_DIR="$INSTALL_DIR/venv"
|
||
readonly VENV_PIP="$VENV_DIR/bin/pip"
|
||
readonly VENV_PYTHON="$VENV_DIR/bin/python"
|
||
readonly CONFIG_DIR="/etc/openhop_repeater"
|
||
readonly LOG_DIR="/var/log/openhop_repeater"
|
||
readonly DATA_DIR="/var/lib/openhop_repeater"
|
||
readonly SERVICE_USER="repeater"
|
||
readonly SERVICE_NAME="openhop-repeater"
|
||
readonly SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}"
|
||
|
||
readonly LEGACY_PYMC_INSTALL_DIR="/opt/pymc_repeater"
|
||
readonly LEGACY_PYMC_CONFIG_DIR="/etc/pymc_repeater"
|
||
readonly LEGACY_PYMC_LOG_DIR="/var/log/pymc_repeater"
|
||
readonly LEGACY_PYMC_DATA_DIR="/var/lib/pymc_repeater"
|
||
|
||
# R2 Wheels Configuration improves install speed on ARM devices
|
||
readonly R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
|
||
readonly R2_ENABLED=1 # Set to 0 to disable R2 wheels and always build from source
|
||
|
||
error() {
|
||
printf 'ERROR: %s\n' "$*" >&2
|
||
}
|
||
|
||
die() {
|
||
error "$*"
|
||
exit 1
|
||
}
|
||
|
||
is_safe_git_ref() {
|
||
local ref="${1:-}"
|
||
[[ "$ref" =~ ^[a-zA-Z0-9._/-]{1,80}$ ]]
|
||
}
|
||
|
||
is_expected_repo_checkout() {
|
||
local source_dir=$1
|
||
local remote_url=""
|
||
|
||
[ -d "${source_dir}/.git" ] || return 1
|
||
remote_url="$(git -C "$source_dir" remote get-url origin 2>/dev/null || true)"
|
||
[ -n "$remote_url" ] || return 1
|
||
case "$remote_url" in
|
||
"$REPOSITORY_URL"|"${REPOSITORY_URL%.git}"|git@github.com:openhop-dev/openhop_repeater.git)
|
||
return 0
|
||
;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
determine_package_source() {
|
||
local script_dir=$1
|
||
local requested_ref=$2
|
||
|
||
if [ -f "${script_dir}/pyproject.toml" ]; then
|
||
printf '%s\n' "$script_dir"
|
||
else
|
||
printf '%s\n' "git+${REPOSITORY_URL}@${requested_ref}"
|
||
fi
|
||
}
|
||
|
||
build_package_requirement() {
|
||
local package_source=$1
|
||
|
||
if [[ "$package_source" == git+* ]]; then
|
||
printf '%s\n' "${PACKAGE_DIST_NAME}[hardware] @ ${package_source}"
|
||
else
|
||
printf '%s\n' "${package_source}[hardware]"
|
||
fi
|
||
}
|
||
|
||
log_package_source() {
|
||
local package_source=$1
|
||
|
||
if [[ "$package_source" == git+* ]]; then
|
||
echo "Using package source: Git URL (${package_source})"
|
||
else
|
||
echo "Using package source: local checkout (${package_source})"
|
||
fi
|
||
}
|
||
|
||
acquire_global_lock() {
|
||
if ! command -v flock >/dev/null 2>&1; then
|
||
error "Required command not found: flock (install util-linux)"
|
||
return 1
|
||
fi
|
||
|
||
mkdir -p "$(dirname "$LOCK_FILE")" || {
|
||
error "Unable to create lock directory for ${LOCK_FILE}"
|
||
return 1
|
||
}
|
||
|
||
exec 9>"$LOCK_FILE" || {
|
||
error "Unable to open lock file ${LOCK_FILE}"
|
||
return 1
|
||
}
|
||
|
||
if ! flock -n 9; then
|
||
error "Another OpenHOP install, upgrade, or uninstall operation is already running"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
run_locked_action() {
|
||
acquire_global_lock || return 1
|
||
"$@"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Virtual-environment helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
cleanup_stale_source_trees() {
|
||
local removed=0
|
||
local path
|
||
|
||
for path in \
|
||
"$INSTALL_DIR/repeater" \
|
||
"$INSTALL_DIR/openhop_core" \
|
||
"$INSTALL_DIR/openhop-repeater" \
|
||
"$INSTALL_DIR/openhop-core" \
|
||
"$LEGACY_PYMC_INSTALL_DIR/repeater" \
|
||
"$LEGACY_PYMC_INSTALL_DIR/pymc_core" \
|
||
"$LEGACY_PYMC_INSTALL_DIR/pymc-repeater" \
|
||
"$LEGACY_PYMC_INSTALL_DIR/pymc-core"
|
||
do
|
||
if [ -e "$path" ]; then
|
||
rm -rf "$path"
|
||
removed=1
|
||
echo " ✓ Removed stale source tree at $path"
|
||
fi
|
||
done
|
||
|
||
if [ "$removed" -eq 0 ]; then
|
||
echo " ✓ No stale source-tree paths found"
|
||
fi
|
||
}
|
||
|
||
cleanup_broken_distribution_metadata() {
|
||
local site_packages_dir
|
||
local removed=0
|
||
|
||
for site_packages_dir in "$VENV_DIR"/lib/python*/site-packages; do
|
||
[ -d "$site_packages_dir" ] || continue
|
||
|
||
while IFS= read -r path; do
|
||
[ -n "$path" ] || continue
|
||
rm -rf "$path"
|
||
removed=1
|
||
echo " ✓ Removed broken package metadata at $path"
|
||
done < <(
|
||
find "$site_packages_dir" \
|
||
\( -type d -name '~*dist-info' -o -type d -name '~*egg-info' -o -type f -name '~*.pth' \) \
|
||
-print 2>/dev/null | sort
|
||
)
|
||
done
|
||
|
||
if [ "$removed" -eq 0 ]; then
|
||
echo " ✓ No broken package metadata found"
|
||
fi
|
||
}
|
||
|
||
migrate_legacy_paths() {
|
||
local timestamp legacy current label backup_path
|
||
timestamp="$(date +%Y%m%d_%H%M%S)"
|
||
|
||
migrate_one_path() {
|
||
legacy="$1"
|
||
current="$2"
|
||
label="$3"
|
||
|
||
if [ ! -e "$legacy" ]; then
|
||
return 0
|
||
fi
|
||
|
||
mkdir -p "$current" 2>/dev/null || true
|
||
|
||
if [ ! -e "$current" ] || [ -z "$(ls -A "$current" 2>/dev/null)" ]; then
|
||
rm -rf "$current" 2>/dev/null || true
|
||
mv "$legacy" "$current"
|
||
echo " ✓ Migrated legacy $label path: $legacy -> $current"
|
||
return 0
|
||
fi
|
||
|
||
cp -an "$legacy"/. "$current"/ 2>/dev/null || true
|
||
backup_path="${legacy}.migrated.${timestamp}"
|
||
mv "$legacy" "$backup_path"
|
||
echo " ✓ Merged legacy $label data into $current"
|
||
echo " ✓ Archived legacy $label path at $backup_path"
|
||
}
|
||
|
||
migrate_one_path "$LEGACY_PYMC_CONFIG_DIR" "$CONFIG_DIR" "config"
|
||
migrate_one_path "$LEGACY_PYMC_LOG_DIR" "$LOG_DIR" "log"
|
||
migrate_one_path "$LEGACY_PYMC_DATA_DIR" "$DATA_DIR" "data"
|
||
migrate_one_path "$LEGACY_PYMC_INSTALL_DIR" "$INSTALL_DIR" "install"
|
||
}
|
||
|
||
ensure_service_user_home() {
|
||
local current_home
|
||
|
||
if ! id "$SERVICE_USER" &>/dev/null; then
|
||
return 0
|
||
fi
|
||
|
||
current_home="$(getent passwd "$SERVICE_USER" | cut -d: -f6)"
|
||
if [ -z "$current_home" ]; then
|
||
return 0
|
||
fi
|
||
|
||
if [ "$current_home" != "$DATA_DIR" ]; then
|
||
mkdir -p "$DATA_DIR" 2>/dev/null || true
|
||
usermod -d "$DATA_DIR" "$SERVICE_USER" 2>/dev/null || true
|
||
echo " ✓ Updated $SERVICE_USER home directory: $current_home -> $DATA_DIR"
|
||
fi
|
||
}
|
||
|
||
# Create (or re-create) the dedicated venv for openhop_repeater
|
||
ensure_venv() {
|
||
local recreate=0
|
||
|
||
if [ ! -x "$VENV_PYTHON" ]; then
|
||
recreate=1
|
||
elif ! "$VENV_PYTHON" -c 'import sys; print(sys.executable)' >/dev/null 2>&1; then
|
||
# Venv python exists but points to a missing interpreter (stale venv).
|
||
recreate=1
|
||
elif ! "$VENV_PYTHON" -m pip --version >/dev/null 2>&1; then
|
||
# Pip script/shebang can break after Python upgrades; treat as stale.
|
||
recreate=1
|
||
fi
|
||
|
||
if [ "$recreate" -eq 1 ]; then
|
||
if [ -d "$VENV_DIR" ]; then
|
||
echo ">>> Rebuilding broken virtual environment at $VENV_DIR ..."
|
||
rm -rf "$VENV_DIR"
|
||
else
|
||
echo ">>> Creating virtual environment at $VENV_DIR ..."
|
||
fi
|
||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||
fi
|
||
|
||
cleanup_broken_distribution_metadata
|
||
|
||
# Always use python -m pip so we don't rely on a potentially stale pip wrapper.
|
||
if ! "$VENV_PYTHON" -m pip install --upgrade pip setuptools wheel; then
|
||
error "Failed to bootstrap pip/setuptools/wheel in venv"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Migrate an existing system-pip install into the venv.
|
||
# Idempotent: safe to call on every upgrade.
|
||
migrate_to_venv() {
|
||
echo ">>> Checking for legacy system-pip installation..."
|
||
|
||
# 1. Ensure the venv exists
|
||
ensure_venv
|
||
ensure_service_user_home
|
||
|
||
# 2. Remove legacy PYTHONPATH from the service unit
|
||
local svc_unit="/etc/systemd/system/openhop-repeater.service"
|
||
if [ -f "$svc_unit" ]; then
|
||
if grep -q 'PYTHONPATH' "$svc_unit" 2>/dev/null; then
|
||
sed -i '/^Environment=.*PYTHONPATH/d' "$svc_unit"
|
||
echo " ✓ Removed legacy PYTHONPATH from service unit"
|
||
fi
|
||
# 3. Fix WorkingDirectory if still pointing at old source
|
||
if grep -q 'WorkingDirectory=/opt/openhop_repeater' "$svc_unit" 2>/dev/null; then
|
||
sed -i 's|WorkingDirectory=/opt/openhop_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$svc_unit"
|
||
echo " ✓ Fixed WorkingDirectory in service unit"
|
||
fi
|
||
if grep -q 'WorkingDirectory=/opt/pymc_repeater\|WorkingDirectory=/var/lib/pymc_repeater' "$svc_unit" 2>/dev/null; then
|
||
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$svc_unit"
|
||
sed -i 's|WorkingDirectory=/var/lib/pymc_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$svc_unit"
|
||
echo " ✓ Migrated legacy WorkingDirectory to openhop path"
|
||
fi
|
||
# 4. Ensure ExecStart uses the venv python
|
||
if grep -q 'ExecStart=/usr/bin/python3' "$svc_unit" 2>/dev/null; then
|
||
sed -i "s|ExecStart=/usr/bin/python3|ExecStart=$VENV_PYTHON|" "$svc_unit"
|
||
echo " ✓ Updated ExecStart to use venv python"
|
||
fi
|
||
if grep -q 'ExecStart=/opt/pymc_repeater/venv/bin/python' "$svc_unit" 2>/dev/null; then
|
||
sed -i "s|ExecStart=/opt/pymc_repeater/venv/bin/python|ExecStart=$VENV_PYTHON|" "$svc_unit"
|
||
echo " ✓ Migrated legacy ExecStart to openhop venv"
|
||
fi
|
||
systemctl daemon-reload
|
||
fi
|
||
|
||
# 5. Remove the package from system python (best-effort)
|
||
python3 -m pip uninstall -y openhop_repeater 2>/dev/null || true
|
||
python3 -m pip uninstall -y openhop_core 2>/dev/null || true
|
||
python3 -m pip uninstall -y pymc_repeater 2>/dev/null || true
|
||
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
|
||
echo " ✓ Cleaned up system-level packages (if any)"
|
||
|
||
# 6. Remove stale source trees that could shadow the venv package
|
||
cleanup_stale_source_trees
|
||
}
|
||
|
||
is_silent_flag() {
|
||
case "${1:-}" in
|
||
--silent|-y|silent) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
is_interactive_flag() {
|
||
case "${1:-}" in
|
||
--interactive|-i|interactive) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# Check if we're running in an interactive terminal
|
||
if [ ! -t 0 ] || [ -z "$TERM" ]; then
|
||
if [[ "${1:-}" =~ ^(upgrade|start|stop|restart)$ ]] && ! is_interactive_flag "${2:-}"; then
|
||
:
|
||
else
|
||
echo "Error: This script requires an interactive terminal."
|
||
echo "Please run from SSH or a local terminal, not via file manager."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# Check if whiptail is available, fallback to dialog
|
||
if command -v whiptail &> /dev/null; then
|
||
DIALOG="whiptail"
|
||
elif command -v dialog &> /dev/null; then
|
||
DIALOG="dialog"
|
||
else
|
||
echo "TUI interface requires whiptail or dialog."
|
||
if [ "$EUID" -eq 0 ]; then
|
||
echo "Installing whiptail..."
|
||
apt-get update -qq && apt-get install -y whiptail
|
||
DIALOG="whiptail"
|
||
else
|
||
echo ""
|
||
echo "Please install whiptail: sudo apt-get install -y whiptail"
|
||
echo "Then run this script again."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# Function to show info box
|
||
show_info() {
|
||
$DIALOG --backtitle "openHop Repeater Management" --title "$1" --msgbox "$2" 12 70
|
||
}
|
||
|
||
# Function to show error box
|
||
show_error() {
|
||
$DIALOG --backtitle "openHop Repeater Management" --title "Error" --msgbox "$1" 8 60
|
||
}
|
||
|
||
# Function to ask yes/no question
|
||
ask_yes_no() {
|
||
$DIALOG --backtitle "openHop Repeater Management" --title "$1" --yesno "$2" 10 70
|
||
}
|
||
|
||
# Function to show progress
|
||
show_progress() {
|
||
echo "$2" | $DIALOG --backtitle "openHop Repeater Management" --title "$1" --gauge "$3" 8 70 0
|
||
}
|
||
|
||
print_live_logs_header() {
|
||
local width=70
|
||
local border
|
||
border="$(printf '%*s' "$width" '' | tr ' ' '=')"
|
||
|
||
printf '\033[1;36m╔%s╗\033[0m\n' "$border"
|
||
printf '\033[1;36m║\033[0m%*s%-*s%*s\033[1;36m║\033[0m\n' 2 '' $((width - 4)) 'openHop Repeater - Live Logs' 2 ''
|
||
printf '\033[1;36m║\033[0m%*s%-*s%*s\033[1;36m║\033[0m\n' 2 '' $((width - 4)) '(Press Ctrl+C to return)' 2 ''
|
||
printf '\033[1;36m╚%s╝\033[0m\n' "$border"
|
||
}
|
||
|
||
# Function to check if service exists
|
||
service_exists() {
|
||
local unit_file="/etc/systemd/system/${SERVICE_NAME}.service"
|
||
|
||
if [ -f "$unit_file" ]; then
|
||
return 0
|
||
fi
|
||
|
||
systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "${SERVICE_NAME}.service"
|
||
}
|
||
|
||
# Function to check if service is installed
|
||
is_installed() {
|
||
[ -d "$INSTALL_DIR" ] && service_exists
|
||
}
|
||
|
||
# Uninstall should be available for partial/broken installs too.
|
||
has_installation_artifacts() {
|
||
[ -d "$INSTALL_DIR" ] \
|
||
|| [ -d "$CONFIG_DIR" ] \
|
||
|| [ -d "$LOG_DIR" ] \
|
||
|| [ -d "$DATA_DIR" ] \
|
||
|| [ -d "$LEGACY_PYMC_INSTALL_DIR" ] \
|
||
|| [ -d "$LEGACY_PYMC_CONFIG_DIR" ] \
|
||
|| [ -d "$LEGACY_PYMC_LOG_DIR" ] \
|
||
|| [ -d "$LEGACY_PYMC_DATA_DIR" ] \
|
||
|| [ -f /etc/systemd/system/openhop-repeater.service ] \
|
||
|| service_exists
|
||
}
|
||
|
||
# Function to check if service is running
|
||
is_running() {
|
||
systemctl is-active "$SERVICE_NAME" >/dev/null 2>&1
|
||
}
|
||
|
||
# Function to check if service is enabled
|
||
is_enabled() {
|
||
systemctl is-enabled "$SERVICE_NAME" >/dev/null 2>&1
|
||
}
|
||
|
||
# Stop/disable legacy service names that can conflict on GPIO.
|
||
disable_legacy_services() {
|
||
local legacy_services="pymc-repeater pymc-repeater.service"
|
||
local svc removed_unit=0
|
||
|
||
for svc in $legacy_services; do
|
||
systemctl stop "$svc" >/dev/null 2>&1 || true
|
||
systemctl disable "$svc" >/dev/null 2>&1 || true
|
||
done
|
||
|
||
if [ -f /etc/systemd/system/pymc-repeater.service ]; then
|
||
rm -f /etc/systemd/system/pymc-repeater.service
|
||
removed_unit=1
|
||
fi
|
||
|
||
if [ "$removed_unit" -eq 1 ]; then
|
||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
wait_for_service_active() {
|
||
local timeout_seconds="${1:-15}"
|
||
local elapsed=0
|
||
|
||
while (( elapsed < timeout_seconds )); do
|
||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
elapsed=$((elapsed + 1))
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
print_service_failure_diagnostics() {
|
||
echo " --- ${SERVICE_NAME} status ---"
|
||
systemctl --no-pager --full status "$SERVICE_NAME" 2>/dev/null || true
|
||
echo " --- recent journal ---"
|
||
journalctl --no-pager -u "$SERVICE_NAME" -n 40 -o cat 2>/dev/null || true
|
||
}
|
||
|
||
# Function to get current version
|
||
get_version() {
|
||
# Read version from the pip-installed package in the venv
|
||
if [ -x "$VENV_PYTHON" ]; then
|
||
"$VENV_PYTHON" -c "from importlib.metadata import version; print(version('openhop_repeater'))" 2>/dev/null \
|
||
|| echo "not installed"
|
||
else
|
||
# Fallback: try system python for pre-migration installs
|
||
python3 -c "from importlib.metadata import version; print(version('openhop_repeater'))" 2>/dev/null \
|
||
|| echo "not installed"
|
||
fi
|
||
}
|
||
|
||
# Function to get service status for display
|
||
get_status_display() {
|
||
if ! is_installed; then
|
||
echo "Not Installed"
|
||
elif is_running; then
|
||
echo "Running ($(get_version))"
|
||
else
|
||
echo "Installed but Stopped ($(get_version))"
|
||
fi
|
||
}
|
||
|
||
# Main menu
|
||
show_main_menu() {
|
||
local status=$(get_status_display)
|
||
|
||
CHOICE=$($DIALOG --backtitle "openHop Repeater Management" --title "openHop Repeater Management" --menu "\nCurrent Status: $status\n\nChoose an action:" 18 70 9 \
|
||
"install" "Install openHop Repeater" \
|
||
"upgrade" "Upgrade existing installation" \
|
||
"reset" "reset existing installation to defaults" \
|
||
"uninstall" "Remove openHop Repeater completely" \
|
||
"config" "Configure radio settings" \
|
||
"start" "Start the service" \
|
||
"stop" "Stop the service" \
|
||
"restart" "Restart the service" \
|
||
"logs" "View live logs" \
|
||
"status" "Show detailed status" \
|
||
"exit" "Exit" 3>&1 1>&2 2>&3)
|
||
|
||
case $CHOICE in
|
||
"install")
|
||
if is_installed; then
|
||
show_error "openHop Repeater is already installed!\n\nUse 'upgrade' to update or 'uninstall' first."
|
||
else
|
||
run_locked_action install_repeater || show_error "Installation failed."
|
||
fi
|
||
;;
|
||
"upgrade")
|
||
if is_installed; then
|
||
run_locked_action upgrade_repeater "false" || show_error "Upgrade failed."
|
||
else
|
||
show_error "openHop Repeater is not installed!\n\nUse 'install' first."
|
||
fi
|
||
;;
|
||
"reset")
|
||
if is_installed; then
|
||
run_locked_action reset_repeater || show_error "Reset failed."
|
||
else
|
||
show_error "openHop Repeater is not installed!\n\nUse 'install' first."
|
||
fi
|
||
;;
|
||
"uninstall")
|
||
if has_installation_artifacts; then
|
||
run_locked_action uninstall_repeater || show_error "Uninstall failed."
|
||
else
|
||
show_error "openHop Repeater is not installed."
|
||
fi
|
||
;;
|
||
"config")
|
||
configure_radio
|
||
;;
|
||
"start")
|
||
manage_service "start" "false"
|
||
;;
|
||
"stop")
|
||
manage_service "stop" "false"
|
||
;;
|
||
"restart")
|
||
manage_service "restart" "false"
|
||
;;
|
||
"logs")
|
||
print_live_logs_header
|
||
echo ""
|
||
journalctl -u "$SERVICE_NAME" -f -o cat --no-hostname | sed -e 's/.*ERROR.*/\x1b[1;31m&\x1b[0m/' -e 's/.*CRITICAL.*/\x1b[1;41;37m&\x1b[0m/' -e 's/.*WARNING.*/\x1b[1;33m&\x1b[0m/' -e 's/.*INFO.*/\x1b[0;32m&\x1b[0m/' -e 's/.*DEBUG.*/\x1b[0;36m&\x1b[0m/'
|
||
;;
|
||
"status")
|
||
show_detailed_status
|
||
;;
|
||
"exit"|"")
|
||
exit 0
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Install function
|
||
install_repeater() {
|
||
# Check root
|
||
if [ "$EUID" -ne 0 ]; then
|
||
show_error "Installation requires root privileges.\n\nPlease run: sudo $0"
|
||
return 1
|
||
fi
|
||
|
||
# Welcome screen (Bypass if the script was passd with the "install" option, assume we want a silent install)
|
||
if [[ "${1:-}" != "install" ]]; then
|
||
$DIALOG --backtitle "openHop Repeater Management" --title "Welcome" --msgbox "\nWelcome to openHop Repeater Setup\n\nThis installer will configure your Linux system as a LoRa mesh network repeater.\n\nPress OK to continue..." 12 70
|
||
fi
|
||
|
||
# SPI Check - Universal approach that works on all boards (skip for CH341 USB-SPI adapter)
|
||
SPI_MISSING=0
|
||
USES_CH341=0
|
||
if [ -f "$CONFIG_DIR/config.yaml" ]; then
|
||
if grep -q "radio_type:.*sx1262_ch341" "$CONFIG_DIR/config.yaml" 2>/dev/null; then
|
||
USES_CH341=1
|
||
fi
|
||
fi
|
||
|
||
if [ "$USES_CH341" -eq 0 ] && ! ls /dev/spidev* >/dev/null 2>&1; then
|
||
# SPI devices not found, check if we're on a Raspberry Pi and can enable it
|
||
CONFIG_FILE=""
|
||
if [ -f "/boot/firmware/config.txt" ]; then
|
||
CONFIG_FILE="/boot/firmware/config.txt"
|
||
elif [ -f "/boot/config.txt" ]; then
|
||
CONFIG_FILE="/boot/config.txt"
|
||
fi
|
||
|
||
if [ -n "$CONFIG_FILE" ]; then
|
||
# Raspberry Pi detected - offer to enable SPI
|
||
if ask_yes_no "SPI Not Enabled" "\nSPI interface is required but not detected (/dev/spidev* not found)!\n\nWould you like to enable it now?\n(This will require a reboot)"; then
|
||
echo "dtparam=spi=on" >> "$CONFIG_FILE"
|
||
show_info "SPI Enabled" "\nSPI has been enabled in $CONFIG_FILE\n\nSystem will reboot now. Please run this script again after reboot."
|
||
reboot
|
||
else
|
||
if ask_yes_no "Continue Without SPI?" "\nSPI is required for LoRa radio operation and is not enabled.\n\nYou can continue the installation, but the radio will not work until SPI is enabled.\n\nContinue anyway?"; then
|
||
SPI_MISSING=1
|
||
else
|
||
show_error "SPI is required for LoRa radio operation.\n\nPlease enable SPI manually and run this script again."
|
||
return
|
||
fi
|
||
fi
|
||
else
|
||
# Not a Raspberry Pi - provide generic instructions
|
||
if ask_yes_no "SPI Not Detected" "\nSPI interface is required but not detected (/dev/spidev* not found).\n\nPlease enable SPI in your system's configuration and ensure the SPI kernel module is loaded.\n\nFor Raspberry Pi: sudo raspi-config -> Interfacing Options -> SPI -> Enable\n\nContinue installation anyway?"; then
|
||
SPI_MISSING=1
|
||
else
|
||
show_error "SPI interface is required but not detected (/dev/spidev* not found).\n\nPlease enable SPI in your system's configuration and ensure the SPI kernel module is loaded.\n\nFor Raspberry Pi: sudo raspi-config -> Interfacing Options -> SPI -> Enable"
|
||
return
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [ "$SPI_MISSING" -eq 1 ]; then
|
||
show_info "Warning" "\nContinuing without SPI enabled.\n\nLoRa radio will not work until SPI is enabled and /dev/spidev* is available."
|
||
fi
|
||
|
||
# Installation progress
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " Installing openHop Repeater"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
|
||
echo ">>> Creating service user..."
|
||
if ! id "$SERVICE_USER" &>/dev/null; then
|
||
useradd --system --home "$DATA_DIR" --shell /sbin/nologin "$SERVICE_USER"
|
||
fi
|
||
ensure_service_user_home
|
||
|
||
disable_legacy_services
|
||
|
||
set +e
|
||
(
|
||
echo "10"; echo "# Adding user to hardware groups..."
|
||
for grp in plugdev dialout gpio i2c spi; do
|
||
getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true
|
||
done
|
||
|
||
echo "20"; echo "# Migrating legacy paths..."
|
||
migrate_legacy_paths
|
||
cleanup_stale_source_trees
|
||
|
||
echo "23"; echo "# Creating directories..."
|
||
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
|
||
|
||
echo "25"; echo "# Installing system dependencies..."
|
||
apt-get update -qq
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo jq python3-pip python3-venv python3-rrdtool wget swig build-essential python3-dev i2c-tools
|
||
# Install polkit (package name varies by distro version)
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \
|
||
|| DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \
|
||
|| echo " Warning: Could not install polkit (sudo fallback will be used)"
|
||
echo "28"; echo "# Creating virtual environment..."
|
||
ensure_venv
|
||
|
||
# Install setuptools_scm in the dedicated venv to avoid PEP 668 system-pip errors.
|
||
"$VENV_PYTHON" -m pip install -q setuptools_scm || true
|
||
|
||
# Install mikefarah yq v4 if not already installed
|
||
if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
|
||
echo ">>> Installing yq..."
|
||
YQ_VERSION="v4.40.5"
|
||
YQ_BINARY="yq_linux_arm64"
|
||
if [[ "$(uname -m)" == "x86_64" ]]; then
|
||
YQ_BINARY="yq_linux_amd64"
|
||
elif [[ "$(uname -m)" == "armv7"* ]]; then
|
||
YQ_BINARY="yq_linux_arm"
|
||
fi
|
||
wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" 2>/dev/null && chmod +x /usr/local/bin/yq
|
||
fi
|
||
|
||
echo "29"; echo "# Installing files..."
|
||
cp "$SCRIPT_DIR/manage.sh" "$INSTALL_DIR/" 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/openhop-repeater.service" "$INSTALL_DIR/" 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/openhop-plugin-manager.service" "$INSTALL_DIR/" 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/radio-settings.json" "$DATA_DIR/" 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true
|
||
|
||
echo "45"; echo "# Installing configuration..."
|
||
cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml.example"
|
||
if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
|
||
cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml"
|
||
fi
|
||
|
||
echo "55"; echo "# Installing systemd service..."
|
||
cp "$SCRIPT_DIR/openhop-repeater.service" /etc/systemd/system/
|
||
if [ -f "$SCRIPT_DIR/openhop-plugin-manager.service" ]; then
|
||
cp "$SCRIPT_DIR/openhop-plugin-manager.service" /etc/systemd/system/openhop-plugin-manager.service
|
||
fi
|
||
systemctl daemon-reload
|
||
|
||
echo "58"; echo "# Installing udev rules for CH341..."
|
||
if [ -f "$SCRIPT_DIR/../openhop-core/99-ch341.rules" ]; then
|
||
cp "$SCRIPT_DIR/../openhop-core/99-ch341.rules" /etc/udev/rules.d/99-ch341.rules
|
||
udevadm control --reload-rules 2>/dev/null || true
|
||
udevadm trigger 2>/dev/null || true
|
||
fi
|
||
|
||
echo "59"; echo "# Installing udev rules for USB modems..."
|
||
if [ -f "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" ]; then
|
||
cp "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" /etc/udev/rules.d/99-openhop-modem.rules
|
||
udevadm control --reload-rules 2>/dev/null || true
|
||
udevadm trigger --subsystem-match=tty --action=change 2>/dev/null || true
|
||
fi
|
||
|
||
echo "65"; echo "# Setting permissions..."
|
||
# Venv stays root-owned (pip runs as root); service user only needs read+execute
|
||
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
|
||
chmod 750 "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
|
||
# Ensure manage.sh and support files in INSTALL_DIR are accessible
|
||
chown root:root "$INSTALL_DIR"
|
||
chmod 755 "$INSTALL_DIR"
|
||
# Ensure the service user can create subdirectories in their home directory
|
||
chmod 755 "$DATA_DIR"
|
||
# Pre-create the .config directory that the service will need
|
||
mkdir -p "$DATA_DIR/.config/openhop_repeater"
|
||
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR/.config"
|
||
|
||
# Configure polkit for passwordless service restart
|
||
|
||
# Work out which version of polkit is installed
|
||
|
||
POLKIT_VERSION=$(pkaction --version 2>/dev/null | awk '{print $NF}')
|
||
if echo "$POLKIT_VERSION" | awk '{ exit ($1 > 0.105) ? 0 : 1 }'; then
|
||
echo "Polkit 0.106 or greater detected, using rules file"
|
||
echo ">>> Configuring polkit for service management..."
|
||
mkdir -p /etc/polkit-1/rules.d
|
||
cat > /etc/polkit-1/rules.d/10-openhop-repeater.rules <<'EOF'
|
||
polkit.addRule(function(action, subject) {
|
||
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
||
action.lookup("unit") == "openhop-repeater.service" &&
|
||
subject.user == "repeater") {
|
||
return polkit.Result.YES;
|
||
}
|
||
});
|
||
EOF
|
||
chmod 0644 /etc/polkit-1/rules.d/10-openhop-repeater.rules
|
||
else
|
||
echo "Polkit 0.105 or less detected, using pkla file"
|
||
mkdir -p /etc/polkit-1/localauthority/50-local.d
|
||
cat > /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla <<'EOF'
|
||
[Allow repeater to restart openhop-repeater service]
|
||
Identity=unix-user:repeater
|
||
Action=org.freedesktop.systemd1.manage-units
|
||
ResultAny=yes
|
||
ResultInactive=yes
|
||
ResultActive=yes
|
||
EOF
|
||
chmod 0644 /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla
|
||
fi
|
||
|
||
# Also configure sudoers as fallback for service restart
|
||
echo ">>> Configuring sudoers for service management..."
|
||
mkdir -p /etc/sudoers.d
|
||
cat > /etc/sudoers.d/openhop-repeater <<'EOF'
|
||
# Allow repeater user to manage the openhop-repeater service without password
|
||
repeater ALL=(root) NOPASSWD: /usr/bin/systemd-run --quiet --collect --unit=openhop-repeater-restart --on-active=2s --timer-property=AccuracySec=100ms --timer-property=RemainAfterElapse=no /usr/bin/systemctl restart openhop-repeater, /usr/bin/systemctl restart openhop-repeater, /usr/bin/systemctl stop openhop-repeater, /usr/bin/systemctl start openhop-repeater, /usr/bin/systemctl status openhop-repeater, /usr/local/bin/pymc-do-upgrade
|
||
EOF
|
||
chmod 0440 /etc/sudoers.d/openhop-repeater
|
||
|
||
echo ">>> Installing OTA upgrade wrapper..."
|
||
cat > /usr/local/bin/pymc-do-upgrade <<'UPGRADEEOF'
|
||
#!/bin/bash -p
|
||
# Ignore caller shell startup hooks, then run with a clean environment.
|
||
exec /usr/bin/env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin HOME=/root LANG=C.UTF-8 PIP_CONFIG_FILE=/dev/null /bin/bash --noprofile --norc -s -- "$@" <<'CLEANENV'
|
||
set -Eeuo pipefail
|
||
umask 022
|
||
cd /
|
||
[[ "$EUID" -eq 0 ]] || { echo "Root is required" >&2; exit 1; }
|
||
[[ "$#" -le 2 ]] || { echo "Too many arguments" >&2; exit 1; }
|
||
# Never repair untrusted executable trees by chowning attacker-owned code.
|
||
require_root_owned() {
|
||
local path="$1" unsafe
|
||
unsafe=$(/usr/bin/find -L "$path" -maxdepth 0 \( ! -uid 0 -o -perm -022 \) -print) || exit 1
|
||
[[ -z "$unsafe" ]] || { echo "Unsafe root execution path: $path" >&2; exit 1; }
|
||
}
|
||
for path in / /opt /opt/openhop_repeater /usr /usr/bin /usr/bin/python3; do
|
||
require_root_owned "$path"
|
||
done
|
||
if [[ -e /opt/openhop_repeater/venv ]]; then
|
||
unsafe=$(/usr/bin/find -L /opt/openhop_repeater/venv \( ! -uid 0 -o -perm -022 \) -print -quit) || exit 1
|
||
[[ -z "$unsafe" ]] || { echo "Unsafe venv; administrator repair required" >&2; exit 1; }
|
||
fi
|
||
CHANNEL="${1:-main}"
|
||
PRETEND_VERSION="${2:-}"
|
||
VENV_DIR="/opt/openhop_repeater/venv"
|
||
VENV_PYTHON="$VENV_DIR/bin/python"
|
||
# Validate: only allow safe git ref characters
|
||
if ! [[ "$CHANNEL" =~ ^[a-zA-Z0-9._/-]{1,80}$ ]]; then
|
||
echo "Invalid channel name: $CHANNEL" >&2
|
||
exit 1
|
||
fi
|
||
if [[ -n "$PRETEND_VERSION" ]] && ! [[ "$PRETEND_VERSION" =~ ^[a-zA-Z0-9][a-zA-Z0-9.!+_-]{0,127}$ ]]; then
|
||
echo "Invalid version" >&2
|
||
exit 1
|
||
fi
|
||
# If caller supplied a version string, tell setuptools_scm to use it (sudo
|
||
# strips env vars so it is passed as a positional argument instead). Scoped to
|
||
# this distribution: the unscoped variable applies to every setuptools_scm
|
||
# project built in the same pip run, so a dependency built from an sdist
|
||
# instead of a wheel would be stamped with our version too.
|
||
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$PRETEND_VERSION"
|
||
# ---- Migration: ensure venv exists (handles upgrades from system-pip era) ----
|
||
if [ ! -x "$VENV_PYTHON" ]; then
|
||
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
|
||
/usr/bin/python3 -I -m venv --system-site-packages "$VENV_DIR"
|
||
"$VENV_PYTHON" -I -m pip install --upgrade pip setuptools wheel
|
||
fi
|
||
# ---- Migration: clean up legacy service unit issues ----
|
||
SVC_UNIT=/etc/systemd/system/openhop-repeater.service
|
||
if grep -q 'PYTHONPATH' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i '/^Environment=.*PYTHONPATH/d' "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'WorkingDirectory=/opt/openhop_repeater' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i 's|WorkingDirectory=/opt/openhop_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'WorkingDirectory=/opt/pymc_repeater\|WorkingDirectory=/var/lib/pymc_repeater' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
|
||
sed -i 's|WorkingDirectory=/var/lib/pymc_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i "s|ExecStart=/usr/bin/python3|ExecStart=$VENV_PYTHON|" "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'ExecStart=/opt/pymc_repeater/venv/bin/python' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i "s|ExecStart=/opt/pymc_repeater/venv/bin/python|ExecStart=$VENV_PYTHON|" "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
# ---- Remove stale source trees that shadow the venv package ----
|
||
[ -d /opt/openhop_repeater/repeater ] && rm -rf /opt/openhop_repeater/repeater
|
||
[ -d /opt/openhop_repeater/openhop-repeater ] && rm -rf /opt/openhop_repeater/openhop-repeater
|
||
[ -d /opt/pymc_repeater/repeater ] && rm -rf /opt/pymc_repeater/repeater
|
||
[ -d /opt/pymc_repeater/pymc-repeater ] && rm -rf /opt/pymc_repeater/pymc-repeater
|
||
# ---- Remove old system-level packages to avoid confusion ----
|
||
/usr/bin/python3 -I -m pip uninstall -y openhop_repeater 2>/dev/null || true
|
||
/usr/bin/python3 -I -m pip uninstall -y openhop_core 2>/dev/null || true
|
||
/usr/bin/python3 -I -m pip uninstall -y pymc_repeater 2>/dev/null || true
|
||
/usr/bin/python3 -I -m pip uninstall -y pymc_core 2>/dev/null || true
|
||
# ---- Try R2 wheels first for faster OTA upgrades ----
|
||
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
|
||
MACHINE_ARCH=$(uname -m)
|
||
case "$MACHINE_ARCH" in
|
||
aarch64) ARCH_TAG="arm64"; PLATFORM_TAG="aarch64" ;;
|
||
armv7l|armv7) ARCH_TAG="armv7"; PLATFORM_TAG="armv7l" ;;
|
||
x86_64) ARCH_TAG="x86_64"; PLATFORM_TAG="x86_64" ;;
|
||
*) ARCH_TAG=""; PLATFORM_TAG="" ;;
|
||
esac
|
||
if [ -n "$ARCH_TAG" ]; then
|
||
PY_TAG=$("$VENV_PYTHON" -I -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311")
|
||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||
echo "[pymc-do-upgrade] Trying dependencies from R2 wheels..."
|
||
"$VENV_PYTHON" -I -m pip install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
|
||
fi
|
||
# ---- Install openhop_repeater from git ----
|
||
if "$VENV_PYTHON" -I -m pip install \
|
||
--upgrade \
|
||
--no-cache-dir \
|
||
"openhop_repeater[hardware] @ git+https://github.com/openhop-dev/openhop_repeater.git@${CHANNEL}"; then
|
||
# Use the just-installed distribution, never a caller checkout or cwd.
|
||
PLUGIN_UNIT=$("$VENV_PYTHON" -I -c 'from importlib.metadata import distribution; print(distribution("openhop_repeater").locate_file("repeater/plugins/openhop-plugin-manager.service"))')
|
||
if [[ -f "$PLUGIN_UNIT" ]]; then
|
||
require_root_owned "$PLUGIN_UNIT"
|
||
install -o root -g root -m 0644 "$PLUGIN_UNIT" /etc/systemd/system/openhop-plugin-manager.service
|
||
systemctl daemon-reload
|
||
systemctl enable openhop-plugin-manager
|
||
# ExecCondition skips startup successfully when plugins.enabled is false.
|
||
systemctl restart openhop-plugin-manager || echo "[pymc-do-upgrade] WARNING: inspect openhop-plugin-manager service status" >&2
|
||
else
|
||
echo "[pymc-do-upgrade] WARNING: selected package does not ship a plugin-manager unit; older channels may not support plugins. Re-run manage.sh upgrade from a plugin-capable release if unexpected." >&2
|
||
fi
|
||
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
|
||
RADIO_BASE_URL="https://raw.githubusercontent.com/openhop-dev/openhop_repeater/${CHANNEL}"
|
||
RADIO_STORAGE_DIR="/var/lib/openhop_repeater"
|
||
mkdir -p "$RADIO_STORAGE_DIR"
|
||
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
|
||
wget -qO "$RADIO_STORAGE_DIR/radio-presets.json" "${RADIO_BASE_URL}/radio-presets.json" 2>/dev/null || true
|
||
else
|
||
exit 1
|
||
fi
|
||
CLEANENV
|
||
UPGRADEEOF
|
||
chown root:root /usr/local/bin/pymc-do-upgrade
|
||
chmod 0755 /usr/local/bin/pymc-do-upgrade
|
||
|
||
echo "75"; echo "# Starting service..."
|
||
systemctl enable "$SERVICE_NAME"
|
||
if [ -f /etc/systemd/system/openhop-plugin-manager.service ]; then
|
||
systemctl enable openhop-plugin-manager 2>/dev/null || true
|
||
fi
|
||
|
||
echo "90"; echo "# Installation files complete..."
|
||
) | "$DIALOG" --backtitle "openHop Repeater Management" --title "Installing" --gauge "Setting up openHop Repeater..." 8 70 0
|
||
local pipeline_status=("${PIPESTATUS[@]}")
|
||
set -e
|
||
|
||
if (( pipeline_status[0] != 0 )); then
|
||
error "Installation setup failed with status ${pipeline_status[0]}"
|
||
return "${pipeline_status[0]}"
|
||
fi
|
||
|
||
if (( pipeline_status[1] != 0 )); then
|
||
error "Installation progress dialog failed with status ${pipeline_status[1]}"
|
||
return "${pipeline_status[1]}"
|
||
fi
|
||
|
||
# Install Python package outside of progress gauge for better error handling
|
||
echo "=== Installing Python Dependencies ==="
|
||
echo ""
|
||
echo "Installing openhop_repeater and dependencies (including openhop_core from PyPI)..."
|
||
echo "This may take a few minutes..."
|
||
echo ""
|
||
|
||
local requested_ref="${OPENHOP_UPGRADE_REF:-$DEFAULT_UPGRADE_REF}"
|
||
if ! is_safe_git_ref "$requested_ref"; then
|
||
error "Invalid upgrade ref: $requested_ref"
|
||
return 1
|
||
fi
|
||
|
||
local package_source
|
||
package_source="$(determine_package_source "$SCRIPT_DIR" "$requested_ref")"
|
||
log_package_source "$package_source"
|
||
|
||
# Only inspect git metadata when this directory is a checkout of the expected repo.
|
||
if is_expected_repo_checkout "$SCRIPT_DIR"; then
|
||
if GIT_VERSION="$(python3 -m setuptools_scm 2>/dev/null)"; then
|
||
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$GIT_VERSION"
|
||
echo "Installing version: $GIT_VERSION"
|
||
fi
|
||
fi
|
||
# We don't have any binary wheels available for these on a LuckFox, so we need to ignore them on that platform.
|
||
if ! grep -q "Luckfox Pico" /proc/device-tree/model 2>/dev/null; then
|
||
# Force binary wheels for slow-to-compile packages (much faster on Raspberry Pi)
|
||
export PIP_ONLY_BINARY=pycryptodome,cffi,PyNaCl,psutil
|
||
fi
|
||
echo "Note: Using optimized binary wheels for faster installation"
|
||
echo ""
|
||
|
||
# Ensure venv exists
|
||
ensure_venv
|
||
|
||
echo "Installing openhop_repeater into venv ($VENV_DIR)..."
|
||
|
||
# Attempt R2 wheels first for faster installation
|
||
if [ "$R2_ENABLED" -eq 1 ]; then
|
||
MACHINE_ARCH=$(uname -m)
|
||
case "$MACHINE_ARCH" in
|
||
aarch64) ARCH_TAG="arm64"; PLATFORM_TAG="aarch64" ;;
|
||
armv7l|armv7) ARCH_TAG="armv7"; PLATFORM_TAG="armv7l" ;;
|
||
x86_64) ARCH_TAG="x86_64"; PLATFORM_TAG="x86_64" ;;
|
||
*) ARCH_TAG=""; PLATFORM_TAG="" ;;
|
||
esac
|
||
if [ -n "$ARCH_TAG" ]; then
|
||
PY_TAG=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311")
|
||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||
echo " Checking for R2 wheels (${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG})..."
|
||
echo " Trying install from R2 pre-built wheels..."
|
||
"$VENV_PYTHON" -m pip install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null && R2_SUCCESS=1 || R2_SUCCESS=0
|
||
if [ "$R2_SUCCESS" -eq 1 ]; then
|
||
echo " ✓ R2 wheels installed"
|
||
else
|
||
echo " - R2 wheels unavailable for this platform/tag, falling back"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
local package_requirement
|
||
package_requirement="$(build_package_requirement "$package_source")"
|
||
if ! "$VENV_PYTHON" -m pip install --upgrade --no-cache-dir "$package_requirement"; then
|
||
error "Python package installation failed"
|
||
return 1
|
||
fi
|
||
echo ""
|
||
echo "✓ Python package installation completed successfully!"
|
||
|
||
# Reload systemd and start the service
|
||
if ! systemctl daemon-reload; then
|
||
error "Failed to reload systemd units"
|
||
return 1
|
||
fi
|
||
if ! systemctl start "$SERVICE_NAME"; then
|
||
error "Failed to start ${SERVICE_NAME}"
|
||
return 1
|
||
fi
|
||
if [ -f /etc/systemd/system/openhop-plugin-manager.service ]; then
|
||
systemctl enable openhop-plugin-manager >/dev/null 2>&1 || true
|
||
systemctl start openhop-plugin-manager >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
# Show final results
|
||
sleep 2
|
||
local ip_address=$(hostname -I | awk '{print $1}')
|
||
if is_running; then
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " ✓ Installation Completed Successfully!"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo "Service is running on:"
|
||
echo " → http://$ip_address:8000"
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " NEXT STEP: Complete Web Setup Wizard"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo "Open the web dashboard in your browser to complete setup:"
|
||
echo ""
|
||
echo " 1. Navigate to: http://$ip_address:8000"
|
||
echo " 2. Complete the 5-step setup wizard:"
|
||
echo " • Choose repeater name"
|
||
echo " • Select hardware board"
|
||
echo " • Configure radio settings"
|
||
echo " • Set admin password"
|
||
echo " 3. Log in to your configured repeater"
|
||
echo ""
|
||
# Container detection: warn about host-side udev rules
|
||
if [ -f /run/host/container-manager ] || [ -n "${container:-}" ] || grep -qsai 'container=' /proc/1/environ 2>/dev/null || [ -f /.dockerenv ]; then
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " ⚠ CONTAINER ENVIRONMENT DETECTED"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo " USB device udev rules do NOT work inside containers."
|
||
echo " You MUST install the CH341 udev rule on the HOST machine:"
|
||
echo ""
|
||
echo " echo 'SUBSYSTEM==\"usb\", ATTR{idVendor}==\"1a86\", ATTR{idProduct}==\"5512\", MODE=\"0666\"' \\"
|
||
echo " | sudo tee /etc/udev/rules.d/99-ch341.rules"
|
||
echo " sudo udevadm control --reload-rules"
|
||
echo " sudo udevadm trigger --subsystem-match=usb --action=change"
|
||
echo ""
|
||
echo " Then unplug and replug the CH341 USB adapter."
|
||
echo ""
|
||
fi
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
if [[ "${1:-}" != "install" ]]; then #Headless install support
|
||
read -p "Press Enter to return to main menu..." || true
|
||
fi
|
||
else
|
||
show_error "Installation completed but service failed to start!\n\nCheck logs from the main menu for details."
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# Reset function
|
||
reset_repeater() {
|
||
local config_file="$CONFIG_DIR/config.yaml"
|
||
local updated_example="$CONFIG_DIR/config.yaml.example"
|
||
|
||
if [ "$EUID" -ne 0 ]; then
|
||
show_error "Upgrade requires root privileges.\n\nPlease run: sudo $0"
|
||
return 1
|
||
fi
|
||
|
||
local current_version=$(get_version)
|
||
|
||
if ask_yes_no "Confirm Reset" "Reset openHop Repeater to default configuration?\n\nContinue?"; then
|
||
|
||
# Show info that upgrade is starting
|
||
show_info "Reseting" "Starting reset process...\n\nProgress will be shown in the terminal."
|
||
|
||
echo "=== Reset Progress ==="
|
||
echo "[1/4] Stopping service..."
|
||
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||
|
||
echo "[2/4] Backing up configuration..."
|
||
if [ -d "$CONFIG_DIR" ]; then
|
||
cp -r "$CONFIG_DIR" "$CONFIG_DIR.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
|
||
echo " ✓ Configuration backed up"
|
||
fi
|
||
echo "3/4 Restore default config.yaml from config.yaml.example"
|
||
cp "$updated_example" "$config_file"
|
||
sleep 5
|
||
# Reload systemd and start the service
|
||
echo "4/4 Restart the service"
|
||
systemctl daemon-reload
|
||
systemctl start "$SERVICE_NAME"
|
||
# Show final results
|
||
sleep 2
|
||
local ip_address=$(hostname -I | awk '{print $1}')
|
||
if is_running; then
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " ✓ Reset Completed Successfully!"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo "Service is running on:"
|
||
echo " → http://$ip_address:8000"
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " NEXT STEP: Complete Web Setup Wizard"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo "Open the web dashboard in your browser to complete setup:"
|
||
echo ""
|
||
echo " 1. Navigate to: http://$ip_address:8000"
|
||
echo " 2. Complete the 5-step setup wizard:"
|
||
echo " • Choose repeater name"
|
||
echo " • Select hardware board"
|
||
echo " • Configure radio settings"
|
||
echo " • Set admin password"
|
||
echo " 3. Log in to your configured repeater"
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
read -p "Press Enter to return to main menu..." || true
|
||
else
|
||
show_error "Installation completed but service failed to start!\n\nCheck logs from the main menu for details."
|
||
return 1
|
||
fi
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Upgrade function
|
||
upgrade_repeater() {
|
||
local silent="${1:-false}"
|
||
local requested_ref="${OPENHOP_UPGRADE_REF:-$DEFAULT_UPGRADE_REF}"
|
||
local package_source=""
|
||
local package_requirement=""
|
||
local service_was_running=0
|
||
local service_was_enabled=0
|
||
local service_stopped=0
|
||
|
||
restart_service_after_failure() {
|
||
if (( service_stopped == 1 )) && (( service_was_running == 1 || service_was_enabled == 1 )); then
|
||
error "Upgrade failed after stopping service; attempting best-effort restart"
|
||
systemctl start "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
if [ "$EUID" -ne 0 ]; then
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "Upgrade requires root privileges. Please run: sudo $0 upgrade"
|
||
else
|
||
show_error "Upgrade requires root privileges.\n\nPlease run: sudo $0"
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
local current_version=$(get_version)
|
||
|
||
if [[ "$silent" != "true" ]]; then
|
||
if ! ask_yes_no "Confirm Upgrade" "Current version: $current_version\n\nThis will upgrade openHop Repeater while preserving your configuration.\n\nContinue?"; then
|
||
return 0
|
||
fi
|
||
|
||
# Show info that upgrade is starting
|
||
show_info "Upgrading" "Starting upgrade process...\n\nThis may take a few minutes.\nProgress will be shown in the terminal."
|
||
else
|
||
echo "Starting upgrade process..."
|
||
echo "Current version: $current_version"
|
||
fi
|
||
|
||
if ! is_safe_git_ref "$requested_ref"; then
|
||
error "Invalid upgrade ref: $requested_ref"
|
||
return 1
|
||
fi
|
||
|
||
package_source="$(determine_package_source "$SCRIPT_DIR" "$requested_ref")"
|
||
package_requirement="$(build_package_requirement "$package_source")"
|
||
log_package_source "$package_source"
|
||
|
||
if is_running; then
|
||
service_was_running=1
|
||
fi
|
||
if is_enabled; then
|
||
service_was_enabled=1
|
||
fi
|
||
|
||
if is_expected_repo_checkout "$SCRIPT_DIR"; then
|
||
if GIT_VERSION="$(python3 -m setuptools_scm 2>/dev/null)"; then
|
||
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$GIT_VERSION"
|
||
echo "Upgrading to version: $GIT_VERSION"
|
||
fi
|
||
fi
|
||
|
||
echo "=== Upgrade Progress ==="
|
||
echo "[1/9] Migrating legacy paths..."
|
||
migrate_legacy_paths
|
||
cleanup_stale_source_trees
|
||
disable_legacy_services
|
||
|
||
echo "[2/9] Ensuring required directories..."
|
||
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
|
||
ensure_service_user_home
|
||
|
||
echo "[3/9] Backing up configuration..."
|
||
if [ -d "$CONFIG_DIR" ]; then
|
||
cp -r "$CONFIG_DIR" "$CONFIG_DIR.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
|
||
echo " ✓ Configuration backed up"
|
||
fi
|
||
|
||
echo "[4/9] Updating system dependencies..."
|
||
apt-get update -qq
|
||
apt-get install -y libffi-dev libusb-1.0-0 sudo jq python3-pip python3-venv python3-rrdtool wget swig build-essential python3-dev i2c-tools
|
||
apt-get install -y policykit-1 2>/dev/null \
|
||
|| apt-get install -y polkitd pkexec 2>/dev/null \
|
||
|| echo " Warning: Could not install polkit (sudo fallback will be used)"
|
||
|
||
# Keep setuptools_scm in the venv instead of system Python (PEP 668 safe).
|
||
ensure_venv
|
||
if ! "$VENV_PYTHON" -m pip install -q setuptools_scm; then
|
||
echo " Warning: Could not install setuptools_scm in venv; continuing without git-derived version"
|
||
fi
|
||
|
||
if ! command -v yq >/dev/null 2>&1 || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
|
||
YQ_VERSION="v4.40.5"
|
||
YQ_BINARY="yq_linux_arm64"
|
||
if [[ "$(uname -m)" == "x86_64" ]]; then
|
||
YQ_BINARY="yq_linux_amd64"
|
||
elif [[ "$(uname -m)" == "armv7"* ]]; then
|
||
YQ_BINARY="yq_linux_arm"
|
||
fi
|
||
wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" && chmod +x /usr/local/bin/yq
|
||
fi
|
||
echo " ✓ Dependencies updated"
|
||
|
||
echo "[5/9] Installing files..."
|
||
if ! cp "$SCRIPT_DIR/openhop-repeater.service" /etc/systemd/system/; then
|
||
echo " ⚠ Warning: Failed to update service file – old service file may remain"
|
||
fi
|
||
if [ -f "$SCRIPT_DIR/openhop-plugin-manager.service" ]; then
|
||
if cp "$SCRIPT_DIR/openhop-plugin-manager.service" /etc/systemd/system/openhop-plugin-manager.service; then
|
||
echo " ✓ openhop-plugin-manager.service installed"
|
||
else
|
||
echo " ⚠ Warning: Failed to install openhop-plugin-manager.service"
|
||
fi
|
||
else
|
||
echo " ⚠ openhop-plugin-manager.service not found in $SCRIPT_DIR (skipped)"
|
||
fi
|
||
cp "$SCRIPT_DIR/radio-settings.json" "$DATA_DIR/" 2>/dev/null || true
|
||
cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true
|
||
echo " ✓ Files updated"
|
||
|
||
echo "[6/9] Validating and updating configuration..."
|
||
if validate_and_update_config; then
|
||
echo " ✓ Configuration validated and updated"
|
||
else
|
||
echo " ⚠ Configuration validation failed, keeping existing config"
|
||
fi
|
||
|
||
echo "[6.5/9] Ensuring user groups and udev rules..."
|
||
for grp in plugdev dialout gpio i2c spi; do
|
||
getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true
|
||
done
|
||
if [ -f "$SCRIPT_DIR/../openhop-core/99-ch341.rules" ]; then
|
||
cp "$SCRIPT_DIR/../openhop-core/99-ch341.rules" /etc/udev/rules.d/99-ch341.rules
|
||
udevadm control --reload-rules 2>/dev/null || true
|
||
udevadm trigger 2>/dev/null || true
|
||
echo " ✓ CH341 udev rules updated"
|
||
elif [ -f /etc/udev/rules.d/99-ch341.rules ]; then
|
||
echo " ✓ CH341 udev rules already present"
|
||
fi
|
||
if [ -f "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" ]; then
|
||
cp "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" /etc/udev/rules.d/99-openhop-modem.rules
|
||
udevadm control --reload-rules 2>/dev/null || true
|
||
udevadm trigger --subsystem-match=tty --action=change 2>/dev/null || true
|
||
echo " ✓ USB modem udev rules updated"
|
||
elif [ -f /etc/udev/rules.d/99-openhop-modem.rules ]; then
|
||
echo " ✓ USB modem udev rules already present"
|
||
fi
|
||
echo " ✓ User groups updated"
|
||
|
||
echo "[7/9] Fixing permissions and helper files..."
|
||
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" 2>/dev/null || true
|
||
chown root:root "$INSTALL_DIR" 2>/dev/null || true
|
||
chmod 755 "$INSTALL_DIR" 2>/dev/null || true
|
||
chmod 750 "$CONFIG_DIR" "$LOG_DIR" 2>/dev/null || true
|
||
chmod 755 "$DATA_DIR" 2>/dev/null || true
|
||
|
||
mkdir -p "$DATA_DIR/.config/openhop_repeater" 2>/dev/null || true
|
||
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR/.config" 2>/dev/null || true
|
||
|
||
POLKIT_VERSION=$(pkaction --version 2>/dev/null | awk '{print $NF}')
|
||
if echo "$POLKIT_VERSION" | awk '{ exit ($1 > 0.105) ? 0 : 1 }'; then
|
||
echo "Polkit 0.106 or greater detected, using rules file"
|
||
echo ">>> Configuring polkit for service management..."
|
||
mkdir -p /etc/polkit-1/rules.d
|
||
cat > /etc/polkit-1/rules.d/10-openhop-repeater.rules <<'EOF'
|
||
polkit.addRule(function(action, subject) {
|
||
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
||
action.lookup("unit") == "openhop-repeater.service" &&
|
||
subject.user == "repeater") {
|
||
return polkit.Result.YES;
|
||
}
|
||
});
|
||
EOF
|
||
chmod 0644 /etc/polkit-1/rules.d/10-openhop-repeater.rules
|
||
else
|
||
echo "Polkit 0.105 or less detected, using pkla file"
|
||
mkdir -p /etc/polkit-1/localauthority/50-local.d
|
||
cat > /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla <<'EOF'
|
||
[Allow repeater to restart openhop-repeater service]
|
||
Identity=unix-user:repeater
|
||
Action=org.freedesktop.systemd1.manage-units
|
||
ResultAny=yes
|
||
ResultInactive=yes
|
||
ResultActive=yes
|
||
EOF
|
||
chmod 0644 /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla
|
||
fi
|
||
|
||
mkdir -p /etc/sudoers.d
|
||
cat > /etc/sudoers.d/openhop-repeater <<'EOF'
|
||
# Allow repeater user to manage the openhop-repeater service without password
|
||
repeater ALL=(root) NOPASSWD: /usr/bin/systemd-run --quiet --collect --unit=openhop-repeater-restart --on-active=2s --timer-property=AccuracySec=100ms --timer-property=RemainAfterElapse=no /usr/bin/systemctl restart openhop-repeater, /usr/bin/systemctl restart openhop-repeater, /usr/bin/systemctl stop openhop-repeater, /usr/bin/systemctl start openhop-repeater, /usr/bin/systemctl status openhop-repeater, /usr/local/bin/pymc-do-upgrade
|
||
EOF
|
||
chmod 0440 /etc/sudoers.d/openhop-repeater
|
||
|
||
cat > /usr/local/bin/pymc-do-upgrade <<'UPGRADEEOF'
|
||
#!/bin/bash -p
|
||
# Ignore caller shell startup hooks, then run with a clean environment.
|
||
exec /usr/bin/env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin HOME=/root LANG=C.UTF-8 PIP_CONFIG_FILE=/dev/null /bin/bash --noprofile --norc -s -- "$@" <<'CLEANENV'
|
||
set -Eeuo pipefail
|
||
umask 022
|
||
cd /
|
||
[[ "$EUID" -eq 0 ]] || { echo "Root is required" >&2; exit 1; }
|
||
[[ "$#" -le 2 ]] || { echo "Too many arguments" >&2; exit 1; }
|
||
# Never repair untrusted executable trees by chowning attacker-owned code.
|
||
require_root_owned() {
|
||
local path="$1" unsafe
|
||
unsafe=$(/usr/bin/find -L "$path" -maxdepth 0 \( ! -uid 0 -o -perm -022 \) -print) || exit 1
|
||
[[ -z "$unsafe" ]] || { echo "Unsafe root execution path: $path" >&2; exit 1; }
|
||
}
|
||
for path in / /opt /opt/openhop_repeater /usr /usr/bin /usr/bin/python3; do
|
||
require_root_owned "$path"
|
||
done
|
||
if [[ -e /opt/openhop_repeater/venv ]]; then
|
||
unsafe=$(/usr/bin/find -L /opt/openhop_repeater/venv \( ! -uid 0 -o -perm -022 \) -print -quit) || exit 1
|
||
[[ -z "$unsafe" ]] || { echo "Unsafe venv; administrator repair required" >&2; exit 1; }
|
||
fi
|
||
CHANNEL="${1:-main}"
|
||
PRETEND_VERSION="${2:-}"
|
||
VENV_DIR="/opt/openhop_repeater/venv"
|
||
VENV_PYTHON="$VENV_DIR/bin/python"
|
||
# Validate: only allow safe git ref characters
|
||
if ! [[ "$CHANNEL" =~ ^[a-zA-Z0-9._/-]{1,80}$ ]]; then
|
||
echo "Invalid channel name: $CHANNEL" >&2
|
||
exit 1
|
||
fi
|
||
if [[ -n "$PRETEND_VERSION" ]] && ! [[ "$PRETEND_VERSION" =~ ^[a-zA-Z0-9][a-zA-Z0-9.!+_-]{0,127}$ ]]; then
|
||
echo "Invalid version" >&2
|
||
exit 1
|
||
fi
|
||
# If caller supplied a version string, tell setuptools_scm to use it (sudo
|
||
# strips env vars so it is passed as a positional argument instead). Scoped to
|
||
# this distribution: the unscoped variable applies to every setuptools_scm
|
||
# project built in the same pip run, so a dependency built from an sdist
|
||
# instead of a wheel would be stamped with our version too.
|
||
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$PRETEND_VERSION"
|
||
# ---- Migration: ensure venv exists (handles upgrades from system-pip era) ----
|
||
if [ ! -x "$VENV_PYTHON" ]; then
|
||
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
|
||
/usr/bin/python3 -I -m venv --system-site-packages "$VENV_DIR"
|
||
"$VENV_PYTHON" -I -m pip install --upgrade pip setuptools wheel
|
||
fi
|
||
# ---- Migration: clean up legacy service unit issues ----
|
||
SVC_UNIT=/etc/systemd/system/openhop-repeater.service
|
||
if grep -q 'PYTHONPATH' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i '/^Environment=.*PYTHONPATH/d' "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'WorkingDirectory=/opt/openhop_repeater' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i 's|WorkingDirectory=/opt/openhop_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'WorkingDirectory=/opt/pymc_repeater\|WorkingDirectory=/var/lib/pymc_repeater' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
|
||
sed -i 's|WorkingDirectory=/var/lib/pymc_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i "s|ExecStart=/usr/bin/python3|ExecStart=$VENV_PYTHON|" "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
if grep -q 'ExecStart=/opt/pymc_repeater/venv/bin/python' "$SVC_UNIT" 2>/dev/null; then
|
||
sed -i "s|ExecStart=/opt/pymc_repeater/venv/bin/python|ExecStart=$VENV_PYTHON|" "$SVC_UNIT"
|
||
systemctl daemon-reload
|
||
fi
|
||
# ---- Remove stale source trees that shadow the venv package ----
|
||
[ -d /opt/openhop_repeater/repeater ] && rm -rf /opt/openhop_repeater/repeater
|
||
[ -d /opt/openhop_repeater/openhop-repeater ] && rm -rf /opt/openhop_repeater/openhop-repeater
|
||
[ -d /opt/pymc_repeater/repeater ] && rm -rf /opt/pymc_repeater/repeater
|
||
[ -d /opt/pymc_repeater/pymc-repeater ] && rm -rf /opt/pymc_repeater/pymc-repeater
|
||
# ---- Remove old system-level packages to avoid confusion ----
|
||
/usr/bin/python3 -I -m pip uninstall -y openhop_repeater 2>/dev/null || true
|
||
/usr/bin/python3 -I -m pip uninstall -y openhop_core 2>/dev/null || true
|
||
/usr/bin/python3 -I -m pip uninstall -y pymc_repeater 2>/dev/null || true
|
||
/usr/bin/python3 -I -m pip uninstall -y pymc_core 2>/dev/null || true
|
||
# ---- Try R2 wheels first for faster OTA upgrades ----
|
||
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
|
||
MACHINE_ARCH=$(uname -m)
|
||
case "$MACHINE_ARCH" in
|
||
aarch64) ARCH_TAG="arm64"; PLATFORM_TAG="aarch64" ;;
|
||
armv7l|armv7) ARCH_TAG="armv7"; PLATFORM_TAG="armv7l" ;;
|
||
x86_64) ARCH_TAG="x86_64"; PLATFORM_TAG="x86_64" ;;
|
||
*) ARCH_TAG=""; PLATFORM_TAG="" ;;
|
||
esac
|
||
if [ -n "$ARCH_TAG" ]; then
|
||
PY_TAG=$("$VENV_PYTHON" -I -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311")
|
||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||
echo "[pymc-do-upgrade] Trying dependencies from R2 wheels..."
|
||
"$VENV_PYTHON" -I -m pip install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
|
||
fi
|
||
# ---- Install openhop_repeater from git ----
|
||
if "$VENV_PYTHON" -I -m pip install \
|
||
--upgrade \
|
||
--no-cache-dir \
|
||
"openhop_repeater[hardware] @ git+https://github.com/openhop-dev/openhop_repeater.git@${CHANNEL}"; then
|
||
# Use the just-installed distribution, never a caller checkout or cwd.
|
||
PLUGIN_UNIT=$("$VENV_PYTHON" -I -c 'from importlib.metadata import distribution; print(distribution("openhop_repeater").locate_file("repeater/plugins/openhop-plugin-manager.service"))')
|
||
if [[ -f "$PLUGIN_UNIT" ]]; then
|
||
require_root_owned "$PLUGIN_UNIT"
|
||
install -o root -g root -m 0644 "$PLUGIN_UNIT" /etc/systemd/system/openhop-plugin-manager.service
|
||
systemctl daemon-reload
|
||
systemctl enable openhop-plugin-manager
|
||
# ExecCondition skips startup successfully when plugins.enabled is false.
|
||
systemctl restart openhop-plugin-manager || echo "[pymc-do-upgrade] WARNING: inspect openhop-plugin-manager service status" >&2
|
||
else
|
||
echo "[pymc-do-upgrade] WARNING: selected package does not ship a plugin-manager unit; older channels may not support plugins. Re-run manage.sh upgrade from a plugin-capable release if unexpected." >&2
|
||
fi
|
||
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
|
||
RADIO_BASE_URL="https://raw.githubusercontent.com/openhop-dev/openhop_repeater/${CHANNEL}"
|
||
RADIO_STORAGE_DIR="/var/lib/openhop_repeater"
|
||
mkdir -p "$RADIO_STORAGE_DIR"
|
||
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
|
||
wget -qO "$RADIO_STORAGE_DIR/radio-presets.json" "${RADIO_BASE_URL}/radio-presets.json" 2>/dev/null || true
|
||
else
|
||
exit 1
|
||
fi
|
||
CLEANENV
|
||
UPGRADEEOF
|
||
chown root:root /usr/local/bin/pymc-do-upgrade
|
||
chmod 0755 /usr/local/bin/pymc-do-upgrade
|
||
echo " ✓ Permissions updated"
|
||
|
||
echo "[8/9] Installing Python Dependencies..."
|
||
|
||
if ! grep -q "Luckfox Pico" /proc/device-tree/model 2>/dev/null; then
|
||
export PIP_ONLY_BINARY=pycryptodome,cffi,PyNaCl,psutil
|
||
fi
|
||
echo "Note: Using optimized binary wheels for faster installation"
|
||
echo ""
|
||
|
||
if ! migrate_to_venv; then
|
||
error "Failed to migrate installation to venv"
|
||
return 1
|
||
fi
|
||
|
||
echo "Upgrading openhop_repeater into venv ($VENV_DIR)..."
|
||
if [ "$R2_ENABLED" -eq 1 ]; then
|
||
MACHINE_ARCH=$(uname -m)
|
||
case "$MACHINE_ARCH" in
|
||
aarch64) ARCH_TAG="arm64"; PLATFORM_TAG="aarch64" ;;
|
||
armv7l|armv7) ARCH_TAG="armv7"; PLATFORM_TAG="armv7l" ;;
|
||
x86_64) ARCH_TAG="x86_64"; PLATFORM_TAG="x86_64" ;;
|
||
*) ARCH_TAG=""; PLATFORM_TAG="" ;;
|
||
esac
|
||
if [ -n "$ARCH_TAG" ]; then
|
||
PY_TAG=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311")
|
||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||
echo " Checking for R2 wheels (${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG})..."
|
||
echo " Trying install from R2 pre-built wheels..."
|
||
if "$VENV_PYTHON" -m pip install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null; then
|
||
echo " ✓ R2 wheels installed"
|
||
else
|
||
echo " - R2 wheels unavailable for this platform/tag, falling back"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if ! "$VENV_PYTHON" -m pip install --upgrade --no-cache-dir "$package_requirement"; then
|
||
error "Package upgrade failed"
|
||
return 1
|
||
fi
|
||
echo ""
|
||
echo "✓ Package and dependencies upgraded successfully!"
|
||
|
||
echo "[9/9] Reloading systemd and restarting service..."
|
||
if ! systemctl daemon-reload; then
|
||
error "Failed to reload systemd"
|
||
return 1
|
||
fi
|
||
|
||
if (( service_was_running == 1 || service_was_enabled == 1 )); then
|
||
if ! systemctl stop "$SERVICE_NAME" 2>/dev/null; then
|
||
error "Failed to stop ${SERVICE_NAME} before restart"
|
||
return 1
|
||
fi
|
||
service_stopped=1
|
||
|
||
if ! systemctl restart "$SERVICE_NAME"; then
|
||
restart_service_after_failure
|
||
error "Failed to restart ${SERVICE_NAME}"
|
||
print_service_failure_diagnostics
|
||
return 1
|
||
fi
|
||
|
||
if ! wait_for_service_active 15; then
|
||
restart_service_after_failure
|
||
error "Service restart completed but ${SERVICE_NAME} is not active"
|
||
print_service_failure_diagnostics
|
||
return 1
|
||
fi
|
||
echo " ✓ Service restarted and active"
|
||
else
|
||
echo " ✓ Service was not running/enabled before upgrade; no restart required"
|
||
fi
|
||
|
||
# Plugin manager is optional but should be installed/enabled on upgrade when present.
|
||
# Failures here must not fail the Repeater upgrade.
|
||
if [ -f /etc/systemd/system/openhop-plugin-manager.service ]; then
|
||
if systemctl enable openhop-plugin-manager >/dev/null 2>&1; then
|
||
echo " ✓ openhop-plugin-manager enabled"
|
||
else
|
||
echo " ⚠ Could not enable openhop-plugin-manager"
|
||
fi
|
||
if systemctl restart openhop-plugin-manager >/dev/null 2>&1; then
|
||
echo " ✓ openhop-plugin-manager restarted"
|
||
elif systemctl start openhop-plugin-manager >/dev/null 2>&1; then
|
||
echo " ✓ openhop-plugin-manager started"
|
||
else
|
||
echo " ⚠ openhop-plugin-manager installed but failed to start (Repeater still OK)"
|
||
systemctl status openhop-plugin-manager --no-pager -l 2>/dev/null | head -20 || true
|
||
fi
|
||
fi
|
||
|
||
local new_version
|
||
new_version=$(get_version)
|
||
|
||
local container_note=""
|
||
if [ -f /run/host/container-manager ] || [ -n "${container:-}" ] || grep -qsai 'container=' /proc/1/environ 2>/dev/null || [ -f /.dockerenv ]; then
|
||
container_note="\n\n⚠ CONTAINER DETECTED:\nUSB udev rules must be set on the HOST, not here.\nCH341 host-side setup: https://docs.openhop.dev/projects/openhop-repeater/hardware-setup/#ch341-usb-spi-hosts"
|
||
fi
|
||
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "Upgrade completed successfully!"
|
||
echo "Version: $current_version -> $new_version"
|
||
if (( service_was_running == 1 || service_was_enabled == 1 )); then
|
||
echo "✓ Service is running"
|
||
fi
|
||
echo "✓ Configuration preserved"
|
||
if [[ -n "$container_note" ]]; then
|
||
printf '%b\n' "$container_note"
|
||
fi
|
||
else
|
||
show_info "Upgrade Complete" "Upgrade completed successfully!\n\nVersion: $current_version → $new_version\n\n✓ Configuration preserved${container_note}"
|
||
fi
|
||
|
||
echo "=== Upgrade Complete ==="
|
||
return 0
|
||
}
|
||
|
||
# Radio Configuration function
|
||
configure_radio() {
|
||
# Check if service is running
|
||
if ! is_running; then
|
||
show_error "Service is not running!\n\nPlease start the service first from the main menu."
|
||
return
|
||
fi
|
||
|
||
# Get IP address
|
||
local ip_address=$(hostname -I | awk '{print $1}')
|
||
|
||
# Show info about web-based configuration
|
||
if ask_yes_no "Configure Radio Settings" "Radio configuration is now done through the web interface.\n\nThe web-based setup wizard provides an easy way to:\n\n• Change repeater name\n• Select hardware board\n• Configure radio frequency and settings\n• Update admin password\n\nWeb Dashboard: http://$ip_address:8000/setup\n\nWould you like to open this information?"; then
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " Web-Based Radio Configuration"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo "To configure your radio settings:"
|
||
echo ""
|
||
echo " 1. Open a web browser"
|
||
echo " 2. Navigate to: http://$ip_address:8000/setup"
|
||
echo " 3. Complete the setup wizard:"
|
||
echo " • Choose repeater name"
|
||
echo " • Select hardware board"
|
||
echo " • Configure radio settings"
|
||
echo " • Update passwords if needed"
|
||
echo " 4. Service will restart automatically with new settings"
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo "Note: The web interface is much easier than the old"
|
||
echo " terminal-based configuration!"
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
read -p "Press Enter to return to main menu..." || true
|
||
fi
|
||
}
|
||
|
||
# Uninstall function
|
||
uninstall_repeater() {
|
||
if [ "$EUID" -ne 0 ]; then
|
||
show_error "Uninstall requires root privileges.\n\nPlease run: sudo $0"
|
||
return 1
|
||
fi
|
||
|
||
if ask_yes_no "Confirm Uninstall" "This will completely remove openHop Repeater including:\n\n- Service and files\n- Configuration (backup will be created)\n- Logs and data\n\nThis action cannot be undone!\n\nContinue?"; then
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo " Uninstalling openHop Repeater"
|
||
echo "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
|
||
echo ">>> Stopping and disabling service..."
|
||
systemctl stop openhop-plugin-manager 2>/dev/null || true
|
||
systemctl disable openhop-plugin-manager 2>/dev/null || true
|
||
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||
systemctl disable "$SERVICE_NAME" 2>/dev/null || true
|
||
|
||
set +e
|
||
(
|
||
echo "20"; echo "# Backing up configuration..."
|
||
if [ -d "$CONFIG_DIR" ]; then
|
||
cp -r "$CONFIG_DIR" "/tmp/openhop_repeater_config_backup_$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
|
||
fi
|
||
|
||
echo "40"; echo "# Removing service files..."
|
||
rm -f /etc/systemd/system/openhop-repeater.service
|
||
rm -f /etc/systemd/system/openhop-plugin-manager.service
|
||
systemctl daemon-reload
|
||
|
||
echo "50"; echo "# Removing polkit and sudoers rules..."
|
||
rm -f /etc/polkit-1/rules.d/10-openhop-repeater.rules || true
|
||
rm -f /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla || true
|
||
rm -f /etc/sudoers.d/openhop-repeater
|
||
rm -f /usr/local/bin/pymc-do-upgrade
|
||
|
||
echo "60"; echo "# Removing installation..."
|
||
rm -rf "$INSTALL_DIR"
|
||
rm -rf "$CONFIG_DIR"
|
||
rm -rf "$LOG_DIR"
|
||
rm -rf "$DATA_DIR"
|
||
rm -rf "$LEGACY_PYMC_INSTALL_DIR"
|
||
rm -rf "$LEGACY_PYMC_CONFIG_DIR"
|
||
rm -rf "$LEGACY_PYMC_LOG_DIR"
|
||
rm -rf "$LEGACY_PYMC_DATA_DIR"
|
||
|
||
echo "80"; echo "# Removing service user..."
|
||
if id "$SERVICE_USER" &>/dev/null; then
|
||
userdel "$SERVICE_USER" 2>/dev/null || true
|
||
fi
|
||
|
||
echo "100"; echo "# Uninstall complete!"
|
||
) | "$DIALOG" --backtitle "openHop Repeater Management" --title "Uninstalling" --gauge "Removing openHop Repeater..." 8 70 0
|
||
local pipeline_status=("${PIPESTATUS[@]}")
|
||
set -e
|
||
|
||
if (( pipeline_status[0] != 0 )); then
|
||
error "Uninstall failed with status ${pipeline_status[0]}"
|
||
return "${pipeline_status[0]}"
|
||
fi
|
||
|
||
if (( pipeline_status[1] != 0 )); then
|
||
error "Uninstall progress dialog failed with status ${pipeline_status[1]}"
|
||
return "${pipeline_status[1]}"
|
||
fi
|
||
|
||
show_info "Uninstall Complete" "\nopenHop Repeater has been completely removed.\n\nConfiguration backup saved to /tmp/\n\nThank you for using openHop Repeater!"
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
# Service management
|
||
manage_service() {
|
||
local action=$1
|
||
local silent="${2:-false}"
|
||
|
||
if [ "$EUID" -ne 0 ]; then
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "Service management requires root privileges. Please run: sudo $0 $action"
|
||
else
|
||
show_error "Service management requires root privileges.\n\nPlease run: sudo $0"
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
if ! service_exists; then
|
||
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
|
||
# Unit file may exist but systemd cache may be stale.
|
||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||
fi
|
||
fi
|
||
|
||
if ! service_exists; then
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "Service is not installed."
|
||
else
|
||
show_error "Service is not installed."
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
case $action in
|
||
"start")
|
||
if ! is_enabled; then
|
||
systemctl enable "$SERVICE_NAME"
|
||
fi
|
||
if ! systemctl start "$SERVICE_NAME"; then
|
||
error "Failed to start ${SERVICE_NAME}"
|
||
return 1
|
||
fi
|
||
if is_running; then
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "✓ openHop Repeater service has been started successfully."
|
||
else
|
||
show_info "Service Started" "\n✓ openHop Repeater service has been started successfully."
|
||
fi
|
||
return 0
|
||
else
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "Failed to start service!"
|
||
echo "Check logs for details."
|
||
else
|
||
show_error "Failed to start service!\n\nCheck logs for details."
|
||
fi
|
||
return 1
|
||
fi
|
||
;;
|
||
"stop")
|
||
if ! systemctl stop "$SERVICE_NAME"; then
|
||
error "Failed to stop ${SERVICE_NAME}"
|
||
return 1
|
||
fi
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "✓ openHop Repeater service has been stopped."
|
||
else
|
||
show_info "Service Stopped" "\n✓ openHop Repeater service has been stopped."
|
||
fi
|
||
return 0
|
||
;;
|
||
"restart")
|
||
if ! systemctl restart "$SERVICE_NAME"; then
|
||
error "Failed to restart ${SERVICE_NAME}"
|
||
return 1
|
||
fi
|
||
if is_running; then
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "✓ openHop Repeater service has been restarted successfully."
|
||
else
|
||
show_info "Service Restarted" "\n✓ openHop Repeater service has been restarted successfully."
|
||
fi
|
||
return 0
|
||
else
|
||
if [[ "$silent" == "true" ]]; then
|
||
echo "Failed to restart service!"
|
||
echo "Check logs for details."
|
||
else
|
||
show_error "Failed to restart service!\n\nCheck logs for details."
|
||
fi
|
||
return 1
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
return 1
|
||
}
|
||
|
||
# Show detailed status
|
||
show_detailed_status() {
|
||
local status_info=""
|
||
local version=$(get_version)
|
||
local ip_address=$(hostname -I | awk '{print $1}')
|
||
|
||
status_info="Installation Status: "
|
||
if is_installed; then
|
||
status_info="${status_info}Installed\n"
|
||
status_info="${status_info}Version: $version\n"
|
||
status_info="${status_info}Install Directory: $INSTALL_DIR\n"
|
||
status_info="${status_info}Config Directory: $CONFIG_DIR\n\n"
|
||
|
||
status_info="${status_info}Service Status: "
|
||
if is_running; then
|
||
status_info="${status_info}Running ✓\n"
|
||
status_info="${status_info}Web Dashboard: http://$ip_address:8000\n\n"
|
||
else
|
||
status_info="${status_info}Stopped ✗\n\n"
|
||
fi
|
||
|
||
# Add system info
|
||
status_info="${status_info}System Info:\n"
|
||
status_info="${status_info}- SPI: "
|
||
if grep -q "spi_bcm2835" /proc/modules 2>/dev/null; then
|
||
status_info="${status_info}Enabled ✓\n"
|
||
else
|
||
status_info="${status_info}Disabled ✗\n"
|
||
fi
|
||
|
||
status_info="${status_info}- IP Address: $ip_address\n"
|
||
status_info="${status_info}- Hostname: $(hostname)\n"
|
||
|
||
else
|
||
status_info="${status_info}Not Installed"
|
||
fi
|
||
|
||
show_info "System Status" "$status_info"
|
||
}
|
||
|
||
# Function to validate and update configuration
|
||
validate_and_update_config() {
|
||
local config_file="$CONFIG_DIR/config.yaml"
|
||
local example_file="$SCRIPT_DIR/config.yaml.example"
|
||
local updated_example="$CONFIG_DIR/config.yaml.example"
|
||
|
||
normalize_legacy_paths_in_config() {
|
||
local target_file="$1"
|
||
[ -f "$target_file" ] || return 0
|
||
|
||
sed -i 's|/var/lib/pymc_repeater|/var/lib/openhop_repeater|g' "$target_file" 2>/dev/null || true
|
||
sed -i 's|/etc/pymc_repeater|/etc/openhop_repeater|g' "$target_file" 2>/dev/null || true
|
||
sed -i 's|/var/log/pymc_repeater|/var/log/openhop_repeater|g' "$target_file" 2>/dev/null || true
|
||
sed -i 's|/opt/pymc_repeater|/opt/openhop_repeater|g' "$target_file" 2>/dev/null || true
|
||
}
|
||
|
||
# Ensure destination config directory exists before copy/merge steps.
|
||
mkdir -p "$CONFIG_DIR"
|
||
|
||
# Copy the new example file
|
||
if [ -f "$example_file" ]; then
|
||
cp "$example_file" "$updated_example"
|
||
else
|
||
echo " ⚠ config.yaml.example not found in source directory"
|
||
return 1
|
||
fi
|
||
|
||
# Check if user config exists
|
||
if [ ! -f "$config_file" ]; then
|
||
echo " ⚠ No existing config.yaml found, copying example"
|
||
cp "$updated_example" "$config_file"
|
||
normalize_legacy_paths_in_config "$config_file"
|
||
return 0
|
||
fi
|
||
|
||
# Check if yq is available
|
||
local YQ_CMD="/usr/local/bin/yq"
|
||
if ! command -v "$YQ_CMD" &> /dev/null; then
|
||
echo " ⚠ mikefarah yq not found at $YQ_CMD, skipping config merge"
|
||
return 0
|
||
fi
|
||
|
||
# Verify it's the correct yq version
|
||
if [[ "$($YQ_CMD --version 2>&1)" != *"mikefarah/yq"* ]]; then
|
||
echo " ⚠ Wrong yq version detected at $YQ_CMD, skipping config merge"
|
||
return 0
|
||
fi
|
||
|
||
echo " Merging configuration..."
|
||
|
||
# Create backup of user config
|
||
local backup_file="${config_file}.backup.$(date +%Y%m%d_%H%M%S)"
|
||
cp "$config_file" "$backup_file"
|
||
echo " ✓ Backup created: $backup_file"
|
||
|
||
# Merge strategy: user config takes precedence, add missing keys from example
|
||
# This uses yq's multiply merge operator (*) which:
|
||
# - Keeps all values from the right operand (user config)
|
||
# - Adds missing keys from the left operand (example config)
|
||
local temp_merged="${config_file}.merged"
|
||
|
||
# Strip comments from user config before merge to prevent comment accumulation.
|
||
# yq preserves comments from both files, so each upgrade cycle would duplicate
|
||
# the header and inline comments. We keep only the example's comments.
|
||
local stripped_user="${config_file}.stripped"
|
||
"$YQ_CMD" eval '... comments=""' "$config_file" > "$stripped_user" 2>/dev/null || cp "$config_file" "$stripped_user"
|
||
|
||
if "$YQ_CMD" eval-all '. as $item ireduce ({}; . * $item)' "$updated_example" "$stripped_user" > "$temp_merged" 2>/dev/null; then
|
||
rm -f "$stripped_user"
|
||
# Verify the merged file is valid YAML
|
||
if "$YQ_CMD" eval '.' "$temp_merged" > /dev/null 2>&1; then
|
||
mv "$temp_merged" "$config_file"
|
||
normalize_legacy_paths_in_config "$config_file"
|
||
echo " ✓ Configuration merged successfully"
|
||
echo " ✓ Legacy pymc_* paths normalized"
|
||
echo " ✓ User settings preserved, new options added"
|
||
return 0
|
||
else
|
||
echo " ✗ Merged config is invalid, restoring backup"
|
||
rm -f "$temp_merged"
|
||
cp "$backup_file" "$config_file"
|
||
normalize_legacy_paths_in_config "$config_file"
|
||
return 1
|
||
fi
|
||
else
|
||
echo " ✗ Config merge failed, keeping original"
|
||
rm -f "$temp_merged" "$stripped_user"
|
||
normalize_legacy_paths_in_config "$config_file"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
usage() {
|
||
echo "openHop Repeater Management Script"
|
||
echo ""
|
||
echo "Usage: $SCRIPT_PATH [action]"
|
||
echo ""
|
||
echo "Actions:"
|
||
echo " install - Install openHop Repeater"
|
||
echo " upgrade - Upgrade existing installation (CLI is silent by default; use --interactive to show dialogs)"
|
||
echo " reset - Reset existing installation to defaults"
|
||
echo " uninstall - Remove openHop Repeater"
|
||
echo " config - Configure radio settings"
|
||
echo " start - Start the service (CLI is silent by default; use --interactive to show dialogs)"
|
||
echo " stop - Stop the service (CLI is silent by default; use --interactive to show dialogs)"
|
||
echo " restart - Restart the service (CLI is silent by default; use --interactive to show dialogs)"
|
||
echo " logs - View live logs"
|
||
echo " status - Show status"
|
||
echo " debug - Show debug information"
|
||
echo ""
|
||
echo "Run without arguments for interactive menu."
|
||
}
|
||
|
||
main() {
|
||
local command="${1:-}"
|
||
local silent_mode="true"
|
||
|
||
case "$command" in
|
||
"" )
|
||
while true; do
|
||
show_main_menu
|
||
done
|
||
;;
|
||
"--help"|"-h")
|
||
usage
|
||
return 0
|
||
;;
|
||
"debug")
|
||
echo "=== Debug Information ==="
|
||
echo "DIALOG: $DIALOG"
|
||
echo "TERM: $TERM"
|
||
echo "TTY: $(tty 2>/dev/null || echo 'not a tty')"
|
||
echo "EUID: $EUID"
|
||
echo "PWD: $PWD"
|
||
echo "Script: $SCRIPT_PATH"
|
||
echo ""
|
||
echo "Testing dialog..."
|
||
"$DIALOG" --backtitle "openHop Repeater Management" --title "Test" --msgbox "Dialog test successful!" 8 40
|
||
echo "Dialog test completed."
|
||
return 0
|
||
;;
|
||
"install"|"upgrade"|"reset"|"uninstall")
|
||
if [ "$EUID" -ne 0 ]; then
|
||
error "${command} requires root privileges. Please run: sudo $SCRIPT_PATH ${command}"
|
||
return 1
|
||
fi
|
||
acquire_global_lock || return 1
|
||
;;
|
||
esac
|
||
|
||
case "$command" in
|
||
"install")
|
||
install_repeater install
|
||
;;
|
||
"upgrade")
|
||
if is_interactive_flag "${2:-}" || [[ "$SILENT_MODE" == "0" || "$SILENT_MODE" == "false" ]]; then
|
||
silent_mode="false"
|
||
fi
|
||
upgrade_repeater "$silent_mode"
|
||
;;
|
||
"reset")
|
||
reset_repeater
|
||
;;
|
||
"uninstall")
|
||
uninstall_repeater
|
||
;;
|
||
"config")
|
||
configure_radio
|
||
;;
|
||
"start"|"stop"|"restart")
|
||
if is_interactive_flag "${2:-}" || [[ "$SILENT_MODE" == "0" || "$SILENT_MODE" == "false" ]]; then
|
||
silent_mode="false"
|
||
fi
|
||
manage_service "$command" "$silent_mode"
|
||
;;
|
||
"logs")
|
||
print_live_logs_header
|
||
echo ""
|
||
journalctl -u "$SERVICE_NAME" -f -o cat --no-hostname | sed -e 's/.*ERROR.*/\x1b[1;31m&\x1b[0m/' -e 's/.*CRITICAL.*/\x1b[1;41;37m&\x1b[0m/' -e 's/.*WARNING.*/\x1b[1;33m&\x1b[0m/' -e 's/.*INFO.*/\x1b[0;32m&\x1b[0m/' -e 's/.*DEBUG.*/\x1b[0;36m&\x1b[0m/'
|
||
;;
|
||
"status")
|
||
show_detailed_status
|
||
;;
|
||
*)
|
||
usage >&2
|
||
return 2
|
||
;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|