#!/usr/bin/env bash # install_service.sh # # Sets up RemoteTerm for MeshCore as a persistent systemd service running as # the current user from the current repo directory. No separate service account # is needed. After installation, git pull and rebuilds work without any sudo -u # gymnastics. # # Run from anywhere inside the repo: # bash scripts/install_service.sh set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' SERVICE_NAME="remoteterm" REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" CURRENT_USER="$(id -un)" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" FRONTEND_MODE="build" echo -e "${BOLD}=== RemoteTerm for MeshCore — Service Installer ===${NC}" echo # ── sanity checks ────────────────────────────────────────────────────────────── if [ "$(uname -s)" != "Linux" ]; then echo -e "${RED}Error: this script is for Linux (systemd) only.${NC}" exit 1 fi if ! command -v systemctl &>/dev/null; then echo -e "${RED}Error: systemd not found. This script requires a systemd-based Linux system.${NC}" exit 1 fi if ! command -v uv &>/dev/null; then echo -e "${RED}Error: 'uv' not found. Install it first:${NC}" echo " curl -LsSf https://astral.sh/uv/install.sh | sh" exit 1 fi if ! command -v python3 &>/dev/null; then echo -e "${RED}Error: python3 is required but was not found.${NC}" exit 1 fi UV_BIN="$(command -v uv)" UVICORN_BIN="$REPO_DIR/.venv/bin/uvicorn" echo -e " Installing as user : ${CYAN}${CURRENT_USER}${NC}" echo -e " Repo directory : ${CYAN}${REPO_DIR}${NC}" echo -e " Service name : ${CYAN}${SERVICE_NAME}${NC}" echo -e " uv : ${CYAN}${UV_BIN}${NC}" echo version_major() { local version="$1" version="${version#v}" printf '%s' "${version%%.*}" } require_minimum_version() { local tool_name="$1" local detected_version="$2" local minimum_major="$3" local major major="$(version_major "$detected_version")" if ! [[ "$major" =~ ^[0-9]+$ ]] || [ "$major" -lt "$minimum_major" ]; then echo -e "${RED}Error: ${tool_name} ${minimum_major}+ is required for a local frontend build, but found ${detected_version}.${NC}" exit 1 fi } # ── transport selection ──────────────────────────────────────────────────────── echo -e "${BOLD}─── Transport ───────────────────────────────────────────────────────${NC}" echo "How is your MeshCore radio connected?" echo " 1) Serial — auto-detect port (default)" echo " 2) Serial — specify port manually" echo " 3) TCP (network connection)" echo " 4) BLE (Bluetooth)" echo read -rp "Select transport [1-4] (default: 1): " TRANSPORT_CHOICE TRANSPORT_CHOICE="${TRANSPORT_CHOICE:-1}" echo NEED_DIALOUT=false SERIAL_PORT="" TCP_HOST="" TCP_PORT="" BLE_ADDRESS="" BLE_PIN="" case "$TRANSPORT_CHOICE" in 1) echo -e "${GREEN}Serial auto-detect selected.${NC}" NEED_DIALOUT=true ;; 2) read -rp "Serial port path (default: /dev/ttyUSB0): " SERIAL_PORT SERIAL_PORT="${SERIAL_PORT:-/dev/ttyUSB0}" echo -e "${GREEN}Serial port: ${SERIAL_PORT}${NC}" NEED_DIALOUT=true ;; 3) read -rp "TCP host (IP address or hostname): " TCP_HOST while [ -z "$TCP_HOST" ]; do echo -e "${RED}TCP host is required.${NC}" read -rp "TCP host: " TCP_HOST done read -rp "TCP port (default: 4000): " TCP_PORT TCP_PORT="${TCP_PORT:-4000}" echo -e "${GREEN}TCP: ${TCP_HOST}:${TCP_PORT}${NC}" ;; 4) read -rp "BLE device address (e.g. AA:BB:CC:DD:EE:FF): " BLE_ADDRESS while [ -z "$BLE_ADDRESS" ]; do echo -e "${RED}BLE address is required.${NC}" read -rp "BLE device address: " BLE_ADDRESS done read -rsp "BLE PIN: " BLE_PIN echo while [ -z "$BLE_PIN" ]; do echo -e "${RED}BLE PIN is required.${NC}" read -rsp "BLE PIN: " BLE_PIN echo done echo -e "${GREEN}BLE: ${BLE_ADDRESS}${NC}" ;; *) echo -e "${YELLOW}Invalid selection — defaulting to serial auto-detect.${NC}" TRANSPORT_CHOICE=1 NEED_DIALOUT=true ;; esac echo # ── frontend install mode ────────────────────────────────────────────────────── echo -e "${BOLD}─── Frontend Assets ─────────────────────────────────────────────────${NC}" echo "How should the frontend be installed?" echo " 1) Build locally with npm (default, latest code, requires node/npm)" echo " 2) Download prebuilt frontend (fastest)" echo read -rp "Select frontend mode [1-2] (default: 1): " FRONTEND_CHOICE FRONTEND_CHOICE="${FRONTEND_CHOICE:-1}" echo case "$FRONTEND_CHOICE" in 1) FRONTEND_MODE="build" echo -e "${GREEN}Using local frontend build.${NC}" ;; 2) FRONTEND_MODE="prebuilt" echo -e "${GREEN}Using prebuilt frontend download.${NC}" ;; *) FRONTEND_MODE="build" echo -e "${YELLOW}Invalid selection — defaulting to local frontend build.${NC}" ;; esac echo # ── bots ────────────────────────────────────────────────────────────────────── echo -e "${BOLD}─── Bot System ──────────────────────────────────────────────────────${NC}" echo -e "${YELLOW}Warning:${NC} The bot system executes arbitrary Python code on the server." echo "It is not recommended on untrusted networks. You can always enable" echo "it later by editing the service file." echo read -rp "Enable bots? [y/N]: " ENABLE_BOTS ENABLE_BOTS="${ENABLE_BOTS:-N}" echo ENABLE_AUTH="N" AUTH_USERNAME="" AUTH_PASSWORD="" if [[ "$ENABLE_BOTS" =~ ^[Yy] ]]; then echo -e "${GREEN}Bots enabled.${NC}" echo echo -e "${BOLD}─── HTTP Basic Auth ─────────────────────────────────────────────────${NC}" echo "With bots enabled, HTTP Basic Auth is strongly recommended if this" echo "service will be accessible beyond your local machine." echo read -rp "Set up HTTP Basic Auth? [Y/n]: " ENABLE_AUTH ENABLE_AUTH="${ENABLE_AUTH:-Y}" echo if [[ "$ENABLE_AUTH" =~ ^[Yy] ]]; then read -rp "Username: " AUTH_USERNAME while [ -z "$AUTH_USERNAME" ]; do echo -e "${RED}Username cannot be empty.${NC}" read -rp "Username: " AUTH_USERNAME done read -rsp "Password: " AUTH_PASSWORD echo while [ -z "$AUTH_PASSWORD" ]; do echo -e "${RED}Password cannot be empty.${NC}" read -rsp "Password: " AUTH_PASSWORD echo done echo -e "${GREEN}Basic Auth configured for user '${AUTH_USERNAME}'.${NC}" echo -e "${YELLOW}Note:${NC} Basic Auth credentials are not safe over plain HTTP." echo "See README_ADVANCED.md for HTTPS setup." fi else echo -e "${GREEN}Bots disabled.${NC}" fi echo # ── python dependencies ──────────────────────────────────────────────────────── echo -e "${YELLOW}Installing Python dependencies (uv sync)...${NC}" cd "$REPO_DIR" uv sync echo -e "${GREEN}Dependencies ready.${NC}" echo # ── frontend assets ──────────────────────────────────────────────────────────── if [ "$FRONTEND_MODE" = "build" ]; then if ! command -v node &>/dev/null; then echo -e "${RED}Error: node is required for a local frontend build but was not found.${NC}" echo -e "${YELLOW}Tip:${NC} Re-run the installer and choose the prebuilt frontend option, or install Node.js 18+ and npm 9+." exit 1 fi if ! command -v npm &>/dev/null; then echo -e "${RED}Error: npm is required for a local frontend build but was not found.${NC}" echo -e "${YELLOW}Tip:${NC} Re-run the installer and choose the prebuilt frontend option, or install Node.js 18+ and npm 9+." exit 1 fi NODE_VERSION="$(node -v)" NPM_VERSION="$(npm -v)" require_minimum_version "Node.js" "$NODE_VERSION" 18 require_minimum_version "npm" "$NPM_VERSION" 9 echo -e "${YELLOW}Building frontend locally with Node ${NODE_VERSION} and npm ${NPM_VERSION}...${NC}" ( cd "$REPO_DIR/frontend" npm install npm run build ) else echo -e "${YELLOW}Fetching prebuilt frontend...${NC}" python3 "$REPO_DIR/scripts/fetch_prebuilt_frontend.py" fi echo # ── data directory ───────────────────────────────────────────────────────────── mkdir -p "$REPO_DIR/data" # ── serial port access ───────────────────────────────────────────────────────── if [ "$NEED_DIALOUT" = true ]; then if ! id -nG "$CURRENT_USER" | grep -qw dialout; then echo -e "${YELLOW}Adding ${CURRENT_USER} to the 'dialout' group for serial port access...${NC}" sudo usermod -aG dialout "$CURRENT_USER" echo -e "${GREEN}Done. You may need to log out and back in for this to take effect for${NC}" echo -e "${GREEN}manual runs; the service itself handles it via SupplementaryGroups.${NC}" echo else echo -e "${GREEN}User ${CURRENT_USER} is already in the 'dialout' group.${NC}" echo fi fi # ── systemd service file ─────────────────────────────────────────────────────── if sudo systemctl is-active --quiet "$SERVICE_NAME"; then echo -e "${YELLOW}${SERVICE_NAME} is currently running; stopping it before applying changes...${NC}" sudo systemctl stop "$SERVICE_NAME" echo fi echo -e "${YELLOW}Writing systemd service file to ${SERVICE_FILE}...${NC}" generate_service_file() { echo "[Unit]" echo "Description=RemoteTerm for MeshCore" echo "After=network.target" echo "" echo "[Service]" echo "Type=simple" echo "User=${CURRENT_USER}" echo "WorkingDirectory=${REPO_DIR}" echo "ExecStart=${UVICORN_BIN} app.main:app --host 0.0.0.0 --port 8000" echo "Restart=always" echo "RestartSec=5" echo "Environment=MESHCORE_DATABASE_PATH=${REPO_DIR}/data/meshcore.db" # Transport case "$TRANSPORT_CHOICE" in 2) echo "Environment=MESHCORE_SERIAL_PORT=${SERIAL_PORT}" ;; 3) echo "Environment=MESHCORE_TCP_HOST=${TCP_HOST}" echo "Environment=MESHCORE_TCP_PORT=${TCP_PORT}" ;; 4) echo "Environment=MESHCORE_BLE_ADDRESS=${BLE_ADDRESS}" echo "Environment=MESHCORE_BLE_PIN=${BLE_PIN}" ;; esac # Bots if [[ ! "$ENABLE_BOTS" =~ ^[Yy] ]]; then echo "Environment=MESHCORE_DISABLE_BOTS=true" fi # Basic auth if [[ "$ENABLE_BOTS" =~ ^[Yy] ]] && [[ "$ENABLE_AUTH" =~ ^[Yy] ]]; then echo "Environment=MESHCORE_BASIC_AUTH_USERNAME=${AUTH_USERNAME}" echo "Environment=MESHCORE_BASIC_AUTH_PASSWORD=${AUTH_PASSWORD}" fi # Serial group access if [ "$NEED_DIALOUT" = true ]; then echo "SupplementaryGroups=dialout" fi echo "" echo "[Install]" echo "WantedBy=multi-user.target" } generate_service_file | sudo tee "$SERVICE_FILE" > /dev/null echo -e "${GREEN}Service file written.${NC}" echo # ── enable and start ─────────────────────────────────────────────────────────── echo -e "${YELLOW}Reloading systemd and applying ${SERVICE_NAME}...${NC}" sudo systemctl daemon-reload sudo systemctl enable "$SERVICE_NAME" sudo systemctl start "$SERVICE_NAME" echo # ── status check ─────────────────────────────────────────────────────────────── echo -e "${YELLOW}Service status:${NC}" sudo systemctl status "$SERVICE_NAME" --no-pager -l || true echo # ── summary ──────────────────────────────────────────────────────────────────── echo -e "${GREEN}${BOLD}=== Installation complete! ===${NC}" echo echo -e "RemoteTerm is running at ${CYAN}http://$(hostname -I | awk '{print $1}'):8000${NC}" echo case "$TRANSPORT_CHOICE" in 1) echo -e " Transport : ${CYAN}Serial (auto-detect)${NC}" ;; 2) echo -e " Transport : ${CYAN}Serial (${SERIAL_PORT})${NC}" ;; 3) echo -e " Transport : ${CYAN}TCP (${TCP_HOST}:${TCP_PORT})${NC}" ;; 4) echo -e " Transport : ${CYAN}BLE (${BLE_ADDRESS})${NC}" ;; esac if [ "$FRONTEND_MODE" = "build" ]; then echo -e " Frontend : ${GREEN}Built locally${NC}" else echo -e " Frontend : ${YELLOW}Prebuilt download${NC}" fi if [[ "$ENABLE_BOTS" =~ ^[Yy] ]]; then echo -e " Bots : ${YELLOW}Enabled${NC}" if [[ "$ENABLE_AUTH" =~ ^[Yy] ]]; then echo -e " Basic Auth: ${GREEN}Enabled (user: ${AUTH_USERNAME})${NC}" else echo -e " Basic Auth: ${YELLOW}Not configured${NC}" fi else echo -e " Bots : ${GREEN}Disabled${NC} (edit ${SERVICE_FILE} to enable)" fi echo if [ "$FRONTEND_MODE" = "prebuilt" ]; then echo -e "${YELLOW}Note:${NC} A prebuilt frontend has been fetched and installed. It may lag" echo "behind the latest code. To build the frontend from source for the most" echo "up-to-date features later, run:" echo echo -e " ${CYAN}cd ${REPO_DIR}/frontend && npm install && npm run build${NC}" echo fi echo -e "${BOLD}─── Quick Reference ─────────────────────────────────────────────────${NC}" echo echo -e "${YELLOW}Update to latest and restart:${NC}" echo -e " cd ${REPO_DIR}" echo -e " git pull" echo -e " uv sync" echo -e " cd frontend && npm install && npm run build && cd .." echo -e " sudo systemctl restart ${SERVICE_NAME}" echo echo -e "${YELLOW}Refresh prebuilt frontend only (skips local build):${NC}" echo -e " python3 ${REPO_DIR}/scripts/fetch_prebuilt_frontend.py" echo -e " sudo systemctl restart ${SERVICE_NAME}" echo echo -e "${YELLOW}View live logs (useful for troubleshooting):${NC}" echo -e " sudo journalctl -u ${SERVICE_NAME} -f" echo echo -e "${YELLOW}Service control:${NC}" echo -e " sudo systemctl start|stop|restart|status ${SERVICE_NAME}" echo -e "${BOLD}─────────────────────────────────────────────────────────────────────${NC}"