#!/usr/bin/env bash set -euo pipefail # Constants readonly SCRIPT_NAME=$(basename "$0") readonly LOG_FILE="/var/log/${SCRIPT_NAME%.*}.log" readonly TMP_DIR=$(mktemp -d -t AIS-catcher-XXXXXX) readonly CONFIG_DIR="/etc/AIS-catcher" readonly PLUGIN_DIR="${CONFIG_DIR}/plugins" readonly DBMS_DIR="${CONFIG_DIR}/DBMS" readonly README_DIR="${CONFIG_DIR}/README" readonly CONFIG_FILE="${CONFIG_DIR}/config.json" readonly CMD_FILE="${CONFIG_DIR}/config.cmd" readonly SERVICE_NAME="ais-catcher.service" readonly SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}" # SDR library paths readonly INCLUDE_DIRS=("/usr/local/include" "/usr/include" "/opt/include" "/opt/local/include") readonly LIBRARY_DIRS=("/usr/local/lib" "/usr/lib" "/opt/lib" "/opt/local/lib") # Global variables DEBUG_BUILD=false INSTALL_PACKAGE=false SERVICE_STATE="" # Logger function log() { local level="$1" shift echo "[$(date +'%Y-%m-%d %H:%M:%S')] [${level}] $*" | tee -a "$LOG_FILE" } # Error handler error_exit() { log "ERROR" "$1" # Try to restore service state before exiting if [[ -n "$SERVICE_STATE" ]]; then log "INFO" "Attempting to restore service state before exit" restore_service_state || log "WARN" "Failed to restore service state during error exit" fi exit 1 } # Cleanup function cleanup() { log "INFO" "Cleaning up temporary directory ${TMP_DIR}" rm -rf "$TMP_DIR" } check_existing_service() { if systemctl is-enabled aiscatcher.service &>/dev/null; then log "ERROR" "Detected existing aiscatcher.service. Please disable it first with:" log "ERROR" "sudo systemctl disable aiscatcher.service" log "ERROR" "Then try running this script again." exit 1 fi } get_system_info() { if [ -f /etc/os-release ]; then . /etc/os-release if [ "$ID" = "raspbian" ]; then OS="debian" else OS="$ID" fi VERSION="$VERSION_CODENAME" else OS=$(uname -s) VERSION=$(uname -r) fi ARCH=$(dpkg --print-architecture) echo "${OS}_${VERSION}_${ARCH}" } # Install dependencies install_dependencies() { local deps="git cmake build-essential pkg-config libzmq3-dev libssl-dev zlib1g-dev libpq-dev unzip libusb-1.0-0-dev libsqlite3-dev" log "INFO" "Installing build dependencies: $deps" apt-get install -y $deps || error_exit "Failed to install dependencies" } # Function to check if a package is available check_package_availability() { local system_info=$1 local packages=( "ais-catcher_debian_bookworm_amd64.deb" "ais-catcher_debian_bookworm_arm64.deb" "ais-catcher_debian_bookworm_armhf.deb" "ais-catcher_debian_bullseye_amd64.deb" "ais-catcher_debian_bullseye_arm64.deb" "ais-catcher_debian_bullseye_armhf.deb" "ais-catcher_debian_trixie_amd64.deb" "ais-catcher_debian_trixie_arm64.deb" "ais-catcher_debian_trixie_armhf.deb" "ais-catcher_ubuntu_focal_amd64.deb" "ais-catcher_ubuntu_jammy_amd64.deb" "ais-catcher_ubuntu_mantic_amd64.deb" "ais-catcher_ubuntu_noble_amd64.deb" ) for package in "${packages[@]}"; do if [[ $package == *"$system_info"* ]]; then echo "$package" return 0 fi done return 1 } # Check for library check_library() { local lib_name="$1" local header_name="$2" local lib_file="$3" log "INFO" "Checking for ${lib_name}" if pkg-config --exists "${lib_name}"; then log "INFO" "${lib_name} found via pkg-config" return 0 fi local header_found=false local library_found=false for dir in "${INCLUDE_DIRS[@]}"; do if [[ -f "${dir}/${header_name}" ]]; then header_found=true break fi done for dir in "${LIBRARY_DIRS[@]}"; do if [[ -f "${dir}/${lib_file}.so" || -f "${dir}/${lib_file}.a" ]]; then library_found=true break fi done if [[ "$header_found" == true && "$library_found" == true ]]; then log "INFO" "${lib_name} found in system directories" return 0 else log "WARN" "${lib_name} not found" return 1 fi } # Install librtlsdr from source install_librtlsdr_from_source() { log "INFO" "Installing librtlsdr from source" git clone https://gitea.osmocom.org/sdr/rtl-sdr --depth 1 || error_exit "Failed to clone rtl-sdr repository" cd rtl-sdr || error_exit "Failed to change directory to rtl-sdr" mkdir build && cd build || error_exit "Failed to create and enter build directory" cmake ../ -DDETACH_KERNEL_DRIVER=ON|| error_exit "Failed to run cmake" make || error_exit "Failed to build rtl-sdr" make install || error_exit "Failed to install rtl-sdr" # Install udev rules with conflict handling install_rtlsdr_udev_rules "../rtl-sdr.rules" ldconfig || error_exit "Failed to run ldconfig" cd ../.. || error_exit "Failed to return to parent directory" } # Function to install RTL-SDR udev rules with conflict detection and refresh install_rtlsdr_udev_rules() { local source_rules="$1" local target_rules="/etc/udev/rules.d/rtl-sdr.rules" log "INFO" "Installing RTL-SDR udev rules" # Check if source rules file exists if [[ ! -f "$source_rules" ]]; then log "ERROR" "Source udev rules file not found: $source_rules" return 1 fi # Check if target rules already exist if [[ -f "$target_rules" ]]; then # Compare checksums to see if files are different if command -v md5sum >/dev/null 2>&1; then local existing_md5=$(md5sum "$target_rules" | cut -d' ' -f1) local new_md5=$(md5sum "$source_rules" | cut -d' ' -f1) if [[ "$existing_md5" = "$new_md5" ]]; then log "INFO" "RTL-SDR udev rules are already up to date" return 0 else log "WARN" "Different RTL-SDR udev rules already exist" log "INFO" "Backing up existing rules to ${target_rules}.backup" cp "$target_rules" "${target_rules}.backup" 2>/dev/null || log "WARN" "Failed to backup existing rules" fi else log "WARN" "RTL-SDR udev rules already exist. Backing up..." cp "$target_rules" "${target_rules}.backup" 2>/dev/null || log "WARN" "Failed to backup existing rules" fi fi # Copy the new rules if cp "$source_rules" "$target_rules" 2>/dev/null; then log "INFO" "RTL-SDR udev rules installed successfully" # Set proper permissions chmod 644 "$target_rules" || log "WARN" "Failed to set permissions on udev rules" # Trigger udev reload reload_udev_rules else error_exit "Failed to install RTL-SDR udev rules" fi } # Function to reload udev rules and trigger device refresh reload_udev_rules() { log "INFO" "Reloading udev rules" # Try different methods to reload udev rules if command -v udevadm >/dev/null 2>&1; then if udevadm control --reload-rules 2>/dev/null; then log "INFO" "Udev rules reloaded successfully" # Trigger udev to reprocess USB devices if udevadm trigger --subsystem-match=usb 2>/dev/null; then log "INFO" "Udev USB devices retriggered" else log "WARN" "Could not retrigger USB devices automatically" suggest_manual_udev_reload fi else log "WARN" "Failed to reload udev rules using udevadm" try_legacy_udev_reload fi else log "WARN" "udevadm not found, trying legacy methods" try_legacy_udev_reload fi } # Function to try legacy udev reload methods try_legacy_udev_reload() { log "INFO" "Attempting legacy udev reload methods" # Try different legacy approaches local reload_success=false # Method 1: Send signal to udevd if killall -USR1 udevd 2>/dev/null; then log "INFO" "Sent reload signal to udevd" reload_success=true elif killall -USR1 systemd-udevd 2>/dev/null; then log "INFO" "Sent reload signal to systemd-udevd" reload_success=true fi # Method 2: Try service restart as last resort if [[ "$reload_success" = false ]]; then if systemctl restart systemd-udevd 2>/dev/null; then log "INFO" "Restarted systemd-udevd service" reload_success=true elif service udev restart 2>/dev/null; then log "INFO" "Restarted udev service" reload_success=true fi fi if [[ "$reload_success" != true ]]; then log "WARN" "All udev reload methods failed" suggest_manual_udev_reload fi } # Function to suggest manual udev reload steps suggest_manual_udev_reload() { log "WARN" "Automatic udev reload failed. To activate RTL-SDR udev rules:" log "INFO" " 1. Disconnect and reconnect your RTL-SDR device" log "INFO" " 2. Run: sudo udevadm control --reload-rules && sudo udevadm trigger" log "INFO" " 3. Or restart the system" log "INFO" " 4. Verify with: rtl_test -t (if device is connected)" } # Install SDR libraries install_sdr_libraries() { local libraries=( "librtlsdr:rtl-sdr.h:librtlsdr" "libairspy:airspy.h:libairspy" "libairspyhf:airspyhf.h:libairspyhf" "libhackrf:hackrf.h:libhackrf" ) for lib_info in "${libraries[@]}"; do IFS=':' read -r lib_name header lib_file <<< "$lib_info" if ! check_library "$lib_name" "$header" "$lib_file"; then case "$lib_name" in librtlsdr) install_librtlsdr_from_source ;; *) log "INFO" "Installing ${lib_name}-dev" apt-get install -y "${lib_name}-dev" || error_exit "Failed to install ${lib_name}-dev" ;; esac fi done } # Build AIS-catcher build_ais_catcher() { log "INFO" "Building AIS-catcher" git clone https://github.com/jvde-github/AIS-catcher.git --depth 1 || error_exit "Failed to download AIS-catcher" cd AIS-catcher if [ "$DEBUG_BUILD" = true ]; then log "INFO" "including debug symbols" ./scripts/build-NMEA2000.sh -d else log "INFO" "excluding debug symbols" ./scripts/build-NMEA2000.sh fi cd .. cp AIS-catcher/build/AIS-catcher /usr/bin || error_exit "Failed to copy AIS-catcher executable" } install_additional_files() { log "INFO" "Installing plugins" mkdir -p "$PLUGIN_DIR" || error_exit "Failed to create plugin directory" cp AIS-catcher/plugins/* "$PLUGIN_DIR" || error_exit "Failed to copy plugins" log "INFO" "Installing DBMS" mkdir -p "$DBMS_DIR" || error_exit "Failed to create DBMS directory" cp AIS-catcher/DBMS/create.sql "$DBMS_DIR" || error_exit "Failed to copy DBMS" log "INFO" "Installing README" mkdir -p "$README_DIR" || error_exit "Failed to create README directory" cp AIS-catcher/README.md "$README_DIR" || error_exit "Failed to copy README" log "INFO" "Copying LICENSE" cp AIS-catcher/LICENSE /etc/AIS-catcher/LICENSE || error_exit "Failed to copy LICENSE" } download_webassets() { local webassets_url="https://github.com/jvde-github/webassets/archive/refs/heads/main.zip" local temp_zip="/tmp/webassets.zip" log "INFO" "Downloading webassets" if curl -sL "$webassets_url" -o "$temp_zip"; then if [[ -d "$CONFIG_DIR/webassets" ]]; then log "INFO" "Removing old webassets" sudo rm -rf "$CONFIG_DIR/webassets" fi log "INFO" "Extracting webassets" sudo unzip -q "$temp_zip" -d "$CONFIG_DIR" sudo mv "$CONFIG_DIR/webassets-main" "$CONFIG_DIR/webassets" rm "$temp_zip" log "INFO" "Webassets updated successfully" else log "ERROR" "Failed to download webassets" return 1 fi } # Setup configuration setup_configuration() { log "INFO" "Setting up configuration files" mkdir -p "$CONFIG_DIR" || error_exit "Failed to create config directory" download_webassets || error_exit "Failed to download webassets" if [[ ! -f "$CONFIG_FILE" ]]; then echo '{"config":"aiscatcher","version":1,"screen":0,"verbose":true,"verbose_time":60,"sharing":true,"udp":[],"tcp":[],"server":{"active":false,"plugin_dir":"/etc/AIS-catcher/plugins","cdn":"/etc/AIS-catcher/webassets","file":"/etc/AIS-catcher/stat.bin","realtime":false,"geojson":false,"prome":false,"port":"8100","backup":"10","context":"settings"},"rtlsdr":{"tuner":"auto","bandwidth":"192K","sample_rate":"1536K","biastee":false,"rtlagc":true},"airspyhf":{"threshold":"low","preamp":false},"rtltcp":{"protocol":"none","sample_rate":"288K","bandwidth":"0","rtlagc":false}}' | tee "$CONFIG_FILE" > /dev/null || error_exit "Failed to create config.json" fi if [[ ! -f "$CMD_FILE" ]]; then cat << EOF | tee "$CMD_FILE" > /dev/null || error_exit "Failed to create config.cmd" # AIS-catcher configuration EOF fi } # Setup systemd service setup_systemd_service() { log "INFO" "Setting up systemd service" cat << EOF | tee "$SERVICE_FILE" > /dev/null || error_exit "Failed to create service file" [Unit] Description=AIS-catcher Service After=network.target [Service] ExecStart=/bin/bash -c '/usr/bin/AIS-catcher -G system on -o 0 -C ${CONFIG_FILE} \$(/bin/grep -v "^#" ${CMD_FILE} | /bin/grep -v "^[[:space:]]*$" | tr -d "\r" | /usr/bin/tr "\n" " ")' Restart=always RestartSec=10 StartLimitBurst=0 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload || error_exit "Failed to reload systemd" } # FIXED: Improved package installation function install_from_package() { local package=$1 local download_url="https://github.com/jvde-github/AIS-catcher/releases/download/Edge/$package" log "INFO" "Installing AIS-catcher from package: $package" # Create a temporary directory with appropriate permissions local temp_dir=$(mktemp -d) chmod 755 "$temp_dir" # Download the package log "INFO" "Downloading package..." if ! curl -sL -o "$temp_dir/$(basename "$download_url")" "$download_url"; then rm -rf "$temp_dir" error_exit "Failed to download package" fi # Ensure the downloaded file has correct permissions chmod 644 "$temp_dir/$package" # Install the package and dependencies using apt with proper flags log "INFO" "Installing package and dependencies..." if ! apt-get update; then rm -rf "$temp_dir" error_exit "Failed to update package lists" fi # Use --allow-downgrades to handle version conflicts and better error handling local install_success=false if apt-get install -y --allow-downgrades "$temp_dir/$package" 2>/dev/null; then install_success=true log "INFO" "Package installed successfully" else log "WARN" "Standard package installation failed, trying with additional flags..." if apt-get install -y --allow-downgrades --allow-change-held-packages "$temp_dir/$package"; then install_success=true log "INFO" "Package installed successfully with additional flags" fi fi # Clean up temporary directory rm -rf "$temp_dir" || log "WARN" "Failed to remove temporary directory: $temp_dir" if [[ "$install_success" != true ]]; then error_exit "Failed to install package after all attempts" fi log "INFO" "AIS-catcher package installation completed" } # Function to stop the service and save its state stop_disable_and_save_service_state() { log "INFO" "Checking AIS-catcher service state, stopping if active, and disabling" local was_active=false local was_enabled=false # Check if the service is active if systemctl is-active --quiet "$SERVICE_NAME"; then was_active=true log "INFO" "$SERVICE_NAME is active, attempting to stop" if systemctl stop "$SERVICE_NAME"; then log "INFO" "$SERVICE_NAME stopped successfully" else log "WARN" "Failed to stop $SERVICE_NAME" fi else log "INFO" "$SERVICE_NAME is not active, no need to stop" fi # Check if the service is enabled if systemctl is-enabled --quiet "$SERVICE_NAME"; then was_enabled=true log "INFO" "$SERVICE_NAME is enabled, attempting to disable" if systemctl disable "$SERVICE_NAME"; then log "INFO" "$SERVICE_NAME disabled successfully" else log "WARN" "Failed to disable $SERVICE_NAME" fi else log "INFO" "$SERVICE_NAME is not enabled, no need to disable" fi # Save the state to the global variable SERVICE_STATE="${was_active},${was_enabled}" log "INFO" "Service state saved: $SERVICE_STATE" } # FIXED: Improved service state restoration with better error handling restore_service_state() { if [[ -z "$SERVICE_STATE" ]]; then log "INFO" "No service state to restore" return 0 fi log "INFO" "Restoring AIS-catcher service state" IFS=',' read -ra STATE_ARRAY <<< "$SERVICE_STATE" local was_active=${STATE_ARRAY[0]:-false} local was_enabled=${STATE_ARRAY[1]:-false} # Enable first if it was enabled if [[ "$was_enabled" == true ]]; then if systemctl enable "$SERVICE_NAME"; then log "INFO" "$SERVICE_NAME enabled successfully" else log "ERROR" "Failed to enable $SERVICE_NAME" return 1 fi fi # Then start if it was active if [[ "$was_active" == true ]]; then if systemctl start "$SERVICE_NAME"; then log "INFO" "$SERVICE_NAME started successfully" # Give service a moment to start and check status sleep 2 if systemctl is-active --quiet "$SERVICE_NAME"; then log "INFO" "$SERVICE_NAME is running properly" else log "WARN" "$SERVICE_NAME may have failed to start properly" systemctl status "$SERVICE_NAME" --no-pager || true fi else log "ERROR" "Failed to start $SERVICE_NAME" return 1 fi fi # Clear the global state after restore SERVICE_STATE="" log "INFO" "Service state restored and cleared" return 0 } # Main function main() { log "INFO" "Starting AIS-catcher installation script" check_existing_service trap cleanup EXIT sudo apt-get update || error_exit "Failed to update package lists" cd "$TMP_DIR" || error_exit "Failed to change to temporary directory" local system_info=$(get_system_info) log "INFO" "Detected system: $system_info" local package=$(check_package_availability "$system_info") if [ $? -eq 0 ]; then log "INFO" "Package available: $package" else log "WARN" "No package available for this system" fi # Parse command-line arguments while [[ $# -gt 0 ]]; do key="$1" case $key in -d|--debug) DEBUG_BUILD=true log "INFO" "Debug build requested" shift # past argument ;; -p|--package) log "INFO" "Package installation requested" INSTALL_PACKAGE=true shift # past argument ;; *) shift # past unrecognized argument ;; esac done # Stop and disable service before any installation stop_disable_and_save_service_state if [ "$INSTALL_PACKAGE" = true ]; then if [ -n "$package" ]; then install_from_package "$package" log "INFO" "AIS-catcher installation from package completed successfully" else error_exit "Package installation requested but no package available for this system. Consider building from source: see https://docs.aiscatcher.org/installation/ubuntu-debian/#install-from-source" fi else log "INFO" "AIS-catcher installation from source" install_dependencies install_sdr_libraries build_ais_catcher install_additional_files fi setup_configuration setup_systemd_service # Restore service state if ! restore_service_state; then log "ERROR" "Failed to restore service state" exit 1 fi log "INFO" "AIS-catcher installation completed successfully" } # Run the script main "$@"