-
Notifications
You must be signed in to change notification settings - Fork 113
Expand file tree
/
Copy pathaiscatcher-install
More file actions
executable file
·1252 lines (1092 loc) · 49.6 KB
/
aiscatcher-install
File metadata and controls
executable file
·1252 lines (1092 loc) · 49.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
#
# AIS-catcher Installation Script
#
# Description:
# Automated installation script for AIS-catcher on Debian and Ubuntu systems.
# Supports both package-based and source-based installations.
#
# Usage:
# sudo ./aiscatcher-install [OPTIONS]
#
# Options:
# -p, --package Install from pre-built package (faster, recommended)
# -i, --interactive Source build with interactive feature selection
# --no-systemd Skip systemd service installation (useful for containers)
# -h, --help Display this help message
#
# Requirements:
# - Must be run as root (use sudo)
# - Debian-based system (Debian/Ubuntu/Raspbian)
# - Internet connection for downloading packages/source
#
# Author: AIS-catcher Project
# License: See LICENSE file
#
set -euo pipefail
# Display usage information
usage() {
cat << EOF
AIS-catcher Installation Script
Usage: sudo $0 [OPTIONS]
Options:
-p, --package Install from pre-built package (faster, recommended)
-i, --interactive Source build with interactive feature selection (forces source path)
-b, --branch Specify git branch to clone (default: main)
--no-systemd Skip systemd service installation (useful for containers)
--no-user Skip user creation and run as root (useful for containers)
--set-reboot-on-failure [BURST [SEC]] Enable reboot watchdog (default: 3 restarts in 1800s)
--unset-reboot-on-failure Disable automatic reboot on repeated failures
--set-auto-restart Enable automatic service restart on crash (Restart=always)
--unset-auto-restart Disable automatic service restart on crash (Restart=no)
-h, --help Display this help message
Examples:
sudo $0 --package # Install from pre-built package
sudo $0 # Build and install from source (main branch)
sudo $0 -i # Source build, asks per-feature what to include
sudo $0 -b develop # Build from develop branch
sudo $0 --no-systemd --no-user # Docker/container installation
sudo $SCRIPT_NAME --set-reboot-on-failure # Enable reboot watchdog (no reinstall needed)
sudo $SCRIPT_NAME --unset-reboot-on-failure # Disable reboot watchdog (no reinstall needed)
sudo $SCRIPT_NAME --set-auto-restart # Enable auto-restart on crash (no reinstall needed)
sudo $SCRIPT_NAME --unset-auto-restart # Disable auto-restart on crash (no reinstall needed)
For more information, visit: https://docs.aiscatcher.org/
EOF
exit 0
}
# Check if running as root
if [ "$EUID" -ne 0 ]; then
_hint_args=$(printf '%q ' "$@")
if [[ -t 1 ]]; then
echo -e "\033[1;31mError: This script must be run as root\033[0m" >&2
echo -e "\033[1;33mPlease run: sudo $0 ${_hint_args% }\033[0m" >&2
else
echo "Error: This script must be run as root" >&2
echo "Please run: sudo $0 ${_hint_args% }" >&2
fi
exit 1
fi
# Constants
SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_NAME
LOG_FILE="/var/log/${SCRIPT_NAME%.*}.log"
readonly LOG_FILE
TMP_DIR=$(mktemp -d -t AIS-catcher-XXXXXX)
readonly TMP_DIR
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 LIB_DIR="/usr/lib/ais-catcher"
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}"
readonly REBOOT_SERVICE_NAME="ais-catcher-reboot.service"
readonly REBOOT_SERVICE_FILE="/etc/systemd/system/${REBOOT_SERVICE_NAME}"
readonly REBOOT_BURST_DEFAULT=3
readonly REBOOT_INTERVAL_DEFAULT=1800
# Global variables
INSTALL_PACKAGE=false
INTERACTIVE=false
SKIP_SYSTEMD=false
SKIP_USER=false
BRANCH="main"
SERVICE_STATE=""
SET_REBOOT=""
REBOOT_BURST="${REBOOT_BURST_DEFAULT}"
REBOOT_INTERVAL="${REBOOT_INTERVAL_DEFAULT}"
SET_AUTO_RESTART=""
INSTALL_REQUESTED=false
# User-facing CMake feature toggles (defaults mirror CMakeLists.txt option() lines).
# Infrastructure libs (SOXR, SAMPLERATE, ZLIB, OPENSSL) and link-style toggles
# (RTLSDR_STATIC, HYDRASDR_STATIC, ARMV6, RBIT) are intentionally not exposed
# interactively — they stay at their CMake defaults.
FEATURE_RTLSDR=ON
FEATURE_AIRSPY=ON
FEATURE_AIRSPYHF=ON
FEATURE_HACKRF=ON
FEATURE_HYDRASDR=ON
FEATURE_SDRPLAY=ON
FEATURE_SOAPYSDR=OFF
FEATURE_ZMQ=ON
FEATURE_NMEA2000=ON
FEATURE_PSQL=ON
FEATURE_SQLITE=ON
FEATURE_WEBVIEWER=ON
# Color constants (using ANSI escape codes)
if [[ -t 1 ]]; then
readonly COLOR_RESET="\033[0m"
readonly COLOR_BOLD="\033[1m"
readonly COLOR_RED="\033[0;31m"
readonly COLOR_GREEN="\033[0;32m"
readonly COLOR_YELLOW="\033[0;33m"
readonly COLOR_BLUE="\033[0;34m"
readonly COLOR_CYAN="\033[0;36m"
readonly COLOR_BOLD_RED="\033[1;31m"
readonly COLOR_BOLD_GREEN="\033[1;32m"
readonly COLOR_BOLD_YELLOW="\033[1;33m"
readonly COLOR_BOLD_BLUE="\033[1;34m"
else
readonly COLOR_RESET=""
readonly COLOR_BOLD=""
readonly COLOR_RED=""
readonly COLOR_GREEN=""
readonly COLOR_YELLOW=""
readonly COLOR_BLUE=""
readonly COLOR_CYAN=""
readonly COLOR_BOLD_RED=""
readonly COLOR_BOLD_GREEN=""
readonly COLOR_BOLD_YELLOW=""
readonly COLOR_BOLD_BLUE=""
fi
# Logger function with color support
log() {
local level="$1"
shift
local color=""
local timestamp="$(date +'%Y-%m-%d %H:%M:%S')"
case "$level" in
ERROR)
color="${COLOR_BOLD_RED}"
;;
WARN)
color="${COLOR_BOLD_YELLOW}"
;;
INFO)
color="${COLOR_BOLD_GREEN}"
;;
*)
color="${COLOR_RESET}"
;;
esac
# Print to terminal with color; %s ensures backslashes in message text are not interpreted
printf "${COLOR_CYAN}[${timestamp}]${COLOR_RESET} ${color}[${level}]${COLOR_RESET} %s\n" "$*"
# Log to file without color codes
printf '[%s] [%s] %s\n' "${timestamp}" "${level}" "$*" >> "$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"
}
# Prompt for a yes/no answer with a default; echoes ON or OFF.
# Args: $1 label, $2 default (ON|OFF)
prompt_yes_no() {
local label="$1" default="$2"
local suffix
if [[ "$default" == "ON" ]]; then suffix="[Y/n]"; else suffix="[y/N]"; fi
while true; do
local answer
read -r -p "$(printf "${COLOR_BOLD}%s${COLOR_RESET} %s: " "$label" "$suffix")" answer
# shellcheck disable=SC2155 # locale lowercasing needs ${var,,}
local lower="${answer,,}"
case "${lower:-${default,,}}" in
on|y|yes|true|1) echo "ON"; return 0 ;;
off|n|no|false|0) echo "OFF"; return 0 ;;
*) echo "Please answer y or n." >&2 ;;
esac
done
}
# Walk the user through per-feature toggles.
# Sets the FEATURE_* globals from current defaults, and aborts if the user
# rejects the summary. Requires a TTY.
interactive_feature_select() {
[[ -t 0 ]] || error_exit "Interactive mode requires a TTY (stdin is not a terminal)"
echo
echo -e "${COLOR_BOLD_BLUE}Interactive feature selection${COLOR_RESET}"
echo -e "Press Enter to accept the default shown in capitals."
echo
echo -e "${COLOR_BOLD}SDR hardware${COLOR_RESET}"
FEATURE_RTLSDR=$(prompt_yes_no " RTL-SDR (RTL2832-based dongles)" "$FEATURE_RTLSDR")
FEATURE_AIRSPY=$(prompt_yes_no " Airspy" "$FEATURE_AIRSPY")
FEATURE_AIRSPYHF=$(prompt_yes_no " Airspy HF+" "$FEATURE_AIRSPYHF")
FEATURE_HACKRF=$(prompt_yes_no " HackRF" "$FEATURE_HACKRF")
FEATURE_HYDRASDR=$(prompt_yes_no " HydraSDR" "$FEATURE_HYDRASDR")
FEATURE_SDRPLAY=$(prompt_yes_no " SDRplay (proprietary SDK required)" "$FEATURE_SDRPLAY")
FEATURE_SOAPYSDR=$(prompt_yes_no " SoapySDR" "$FEATURE_SOAPYSDR")
echo
echo -e "${COLOR_BOLD}Inputs / outputs${COLOR_RESET}"
FEATURE_ZMQ=$(prompt_yes_no " ZMQ (-z input / ZMQ output)" "$FEATURE_ZMQ")
FEATURE_NMEA2000=$(prompt_yes_no " NMEA2000 socketCAN (-i / -I)" "$FEATURE_NMEA2000")
FEATURE_PSQL=$(prompt_yes_no " PostgreSQL output (-D)" "$FEATURE_PSQL")
FEATURE_SQLITE=$(prompt_yes_no " SQLite" "$FEATURE_SQLITE")
echo
echo -e "${COLOR_BOLD}Other${COLOR_RESET}"
FEATURE_WEBVIEWER=$(prompt_yes_no " Embedded web UI (-N)" "$FEATURE_WEBVIEWER")
echo
echo -e "${COLOR_BOLD_BLUE}Selected features:${COLOR_RESET}"
local f val
for f in RTLSDR AIRSPY AIRSPYHF HACKRF HYDRASDR SDRPLAY SOAPYSDR ZMQ NMEA2000 PSQL SQLITE WEBVIEWER; do
val=$(eval "echo \$FEATURE_$f")
printf " %-12s %s\n" "$f" "$val"
done
echo
local confirm
read -r -p "Proceed with these settings? [Y/n]: " confirm
if [[ "${confirm,,}" =~ ^(n|no)$ ]]; then
log "INFO" "Installation aborted by user"
exit 0
fi
}
# Cancel any pending reboot and stop the watchdog service.
# Safe to call unconditionally — harmless if nothing is pending or running.
cancel_reboot_watchdog() {
shutdown -c 2>/dev/null || true
systemctl stop "$REBOOT_SERVICE_NAME" 2>/dev/null || true
log "INFO" "Reboot watchdog cancelled (pending shutdown cleared, service stopped)"
}
# Replace a directive if present in FILE, otherwise insert it under its section header.
sed_replace_or_insert() {
local key="$1" val="$2" section="$3" file="$4"
if grep -q "^${key}=" "$file"; then
sed -i "s/^${key}=.*/${key}=${val}/" "$file"
else
sed -i "/^\[${section}\]/a ${key}=${val}" "$file"
fi
}
# Reload systemd and reset the service failure counter.
reload_and_reset_service() {
systemctl daemon-reload || error_exit "Failed to reload systemd"
log "INFO" "Resetting failure counter for ${SERVICE_NAME}"
systemctl reset-failed "$SERVICE_NAME" 2>/dev/null || log "WARN" "Failed to reset failure counter"
}
# Log whether new policy settings will take effect now or at next restart.
# Neither toggle handler restarts the service, so we just check current state for messaging.
log_service_policy_notice() {
local policy_desc="$1"
if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
log "INFO" "${SERVICE_NAME} is running — ${policy_desc} will apply from the next restart"
else
log "INFO" "${SERVICE_NAME} was not running — configuration updated"
fi
}
################################################################################
# REBOOT-ON-FAILURE TOGGLE (standalone, no reinstall needed)
################################################################################
# Enable or disable the reboot watchdog by updating StartLimitBurst/Interval in the service file
handle_reboot_setting() {
if [[ ! -f "$SERVICE_FILE" ]]; then
error_exit "Service file not found at ${SERVICE_FILE} — is AIS-catcher installed?"
fi
if [[ "$SET_REBOOT" == "true" ]]; then
sed_replace_or_insert StartLimitBurst "${REBOOT_BURST}" Unit "$SERVICE_FILE"
sed_replace_or_insert StartLimitIntervalSec "${REBOOT_INTERVAL}" Unit "$SERVICE_FILE"
# Add OnFailure= if not already present
if ! grep -q "^OnFailure=" "$SERVICE_FILE"; then
sed -i "/^After=network.target/a OnFailure=${REBOOT_SERVICE_NAME}" "$SERVICE_FILE"
fi
log "INFO" "Reboot on failure enabled (burst: ${REBOOT_BURST}, interval: ${REBOOT_INTERVAL}s)"
else
sed_replace_or_insert StartLimitBurst 0 Unit "$SERVICE_FILE"
sed_replace_or_insert StartLimitIntervalSec 0 Unit "$SERVICE_FILE"
# Remove OnFailure= line — with burst=0 it fires on every crash, not just limit-hit
sed -i "/^OnFailure=/d" "$SERVICE_FILE"
log "INFO" "Reboot on failure disabled"
fi
reload_and_reset_service
# No restart needed: Restart=, StartLimitBurst=, OnFailure= are systemd policy directives
# that take effect on the next crash/start — daemon-reload is sufficient.
log_service_policy_notice "new watchdog settings"
}
################################################################################
# AUTO-RESTART TOGGLE (standalone, no reinstall needed)
################################################################################
# Enable or disable automatic service restart by updating Restart= in the service file
handle_auto_restart_setting() {
if [[ ! -f "$SERVICE_FILE" ]]; then
error_exit "Service file not found at ${SERVICE_FILE} — is AIS-catcher installed?"
fi
if [[ "$SET_AUTO_RESTART" == "true" ]]; then
sed_replace_or_insert Restart always Service "$SERVICE_FILE"
log "INFO" "Auto-restart enabled (Restart=always)"
else
sed_replace_or_insert Restart no Service "$SERVICE_FILE"
log "INFO" "Auto-restart disabled (Restart=no)"
fi
reload_and_reset_service
# No restart needed: Restart= is a systemd policy directive that takes effect on the
# next crash/start — daemon-reload is sufficient and avoids unnecessary interruption.
log_service_policy_notice "new restart policy"
}
################################################################################
# UTILITY FUNCTIONS (Used by both package and source installations)
################################################################################
# Check for conflicting existing service
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" "systemctl disable aiscatcher.service"
log "ERROR" "Then run this script again."
exit 1
fi
}
# Detect system information (OS, version, architecture)
get_system_info() {
local os version arch
if [ -f /etc/os-release ]; then
. /etc/os-release
if [ "${ID:-}" = "raspbian" ]; then
os="debian"
else
os="${ID:-}"
fi
version="${VERSION_CODENAME:-}"
[[ -z "$os" ]] && error_exit "Could not determine OS ID from /etc/os-release"
[[ -z "$version" ]] && error_exit "Could not determine VERSION_CODENAME from /etc/os-release"
else
os=$(uname -s) || error_exit "Failed to determine OS name (uname -s)"
version=$(uname -r) || error_exit "Failed to determine OS version (uname -r)"
fi
arch=$(dpkg --print-architecture) || error_exit "Failed to determine system architecture (dpkg --print-architecture)"
echo "${os}_${version}_${arch}"
}
################################################################################
# SOURCE INSTALLATION FUNCTIONS
################################################################################
# Install build dependencies for source compilation.
# OpenSSL/zlib stay required (always-on infrastructure features). Per-feature
# optional packages are only installed when the matching FEATURE_* is ON.
install_source_build_dependencies() {
local required_deps="git cmake build-essential pkg-config libssl-dev zlib1g-dev libusb-1.0-0-dev curl"
local optional_deps=""
[[ "$FEATURE_ZMQ" == "ON" ]] && optional_deps+=" libzmq3-dev"
[[ "$FEATURE_PSQL" == "ON" ]] && optional_deps+=" libpq-dev"
[[ "$FEATURE_SQLITE" == "ON" ]] && optional_deps+=" libsqlite3-dev"
[[ "$FEATURE_AIRSPY" == "ON" ]] && optional_deps+=" libairspy-dev"
[[ "$FEATURE_AIRSPYHF" == "ON" ]] && optional_deps+=" libairspyhf-dev"
[[ "$FEATURE_HACKRF" == "ON" ]] && optional_deps+=" libhackrf-dev"
[[ "$FEATURE_SOAPYSDR" == "ON" ]] && optional_deps+=" libsoapysdr-dev"
log "INFO" "Installing required build dependencies: $required_deps"
apt-get install -y $required_deps || error_exit "Failed to install required dependencies"
if [[ -n "$optional_deps" ]]; then
log "INFO" "Installing optional dependencies (failures allowed):$optional_deps"
for pkg in $optional_deps; do
if apt-get install -y "$pkg"; then
log "INFO" "Installed $pkg"
else
log "WARN" "Could not install $pkg - corresponding feature may be unavailable"
fi
done
else
log "INFO" "No optional dependencies needed for the selected feature set"
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 ../ -DCMAKE_INSTALL_PREFIX="${LIB_DIR}" -DDETACH_KERNEL_DRIVER=ON -DINSTALL_UDEV_RULES=ON || error_exit "Failed to run cmake"
make -j"$(nproc)" || error_exit "Failed to build rtl-sdr"
make install || error_exit "Failed to install rtl-sdr"
# ldconfig not needed: libraries are in a custom prefix and the RPATH in the AIS-catcher
# binary points directly to ${LIB_DIR}/lib, so the dynamic linker finds them without it.
cd ../.. || error_exit "Failed to return to parent directory"
}
# Install libhydrasdr from source
install_libhydrasdr_from_source() {
log "INFO" "Installing libhydrasdr from source"
git clone https://github.com/hydrasdr/rfone_host.git --depth 1 || error_exit "Failed to clone rfone_host repository"
cd rfone_host/libhydrasdr || error_exit "Failed to change directory to rfone_host/libhydrasdr"
mkdir build && cd build || error_exit "Failed to create and enter build directory"
cmake .. -DCMAKE_INSTALL_PREFIX="${LIB_DIR}" -DINSTALL_UDEV_RULES=ON || error_exit "Failed to run cmake"
make -j"$(nproc)" || error_exit "Failed to build libhydrasdr"
make install || error_exit "Failed to install libhydrasdr"
cd ../../.. || error_exit "Failed to return to parent directory"
}
# Install NMEA2000 library from source
install_nmea2000_from_source() {
log "INFO" "Building NMEA2000 library from source"
git clone https://github.com/jvde-github/NMEA2000.git --depth 1 || error_exit "Failed to clone NMEA2000 repository"
cd NMEA2000/src || error_exit "Failed to change directory to NMEA2000/src"
# Build as static library
log "INFO" "Building NMEA2000 as static library"
g++ -O3 -c N2kMsg.cpp N2kStream.cpp N2kMessages.cpp N2kTimer.cpp NMEA2000.cpp N2kGroupFunctionDefaultHandlers.cpp N2kGroupFunction.cpp -I. || error_exit "Failed to compile NMEA2000"
ar rcs libnmea2000.a *.o || error_exit "Failed to create NMEA2000 static library"
cd ../.. || error_exit "Failed to return to parent directory"
}
# Install SDR libraries from source (only for source build).
# Each step is gated on its FEATURE_* flag so users disabling a feature
# don't pay the clone+build cost or pollute ${LIB_DIR}.
install_sdr_libraries_from_source() {
# Create library directory
mkdir -p "${LIB_DIR}" || error_exit "Failed to create library directory"
# rtlsdr and hydrasdr from source (need custom cmake flags for udev rules)
[[ "$FEATURE_RTLSDR" == "ON" ]] && install_librtlsdr_from_source
[[ "$FEATURE_HYDRASDR" == "ON" ]] && install_libhydrasdr_from_source
# NMEA2000 library (built only if the feature is enabled)
[[ "$FEATURE_NMEA2000" == "ON" ]] && install_nmea2000_from_source
# Explicit success: under `set -e`, a trailing `[[ ]] && fn` whose test is
# false makes the whole function return 1 (the && short-circuits, leaving
# the test's exit code as the function's last status). That silently kills
# the script before build_ais_catcher_from_source ever runs.
return 0
}
# Build AIS-catcher from source
build_ais_catcher_from_source() {
log "INFO" "Building AIS-catcher from branch: ${BRANCH}"
git clone https://github.com/jvde-github/AIS-catcher.git --depth 1 --branch "${BRANCH}" || error_exit "Failed to download AIS-catcher (branch: ${BRANCH})"
cd AIS-catcher
# Set environment variables to find custom libraries
export PKG_CONFIG_PATH="${LIB_DIR}/lib/pkgconfig:${PKG_CONFIG_PATH:-}"
export CMAKE_PREFIX_PATH="${LIB_DIR}:${CMAKE_PREFIX_PATH:-}"
# Build with rpath to custom library directory
mkdir -p build && cd build
local cmake_args=(
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_INSTALL_RPATH="${LIB_DIR}/lib;${LIB_DIR}"
-DCMAKE_BUILD_RPATH="${LIB_DIR}/lib;${LIB_DIR}"
)
# Only point at the source-built NMEA2000 archive when that feature is enabled.
if [[ "$FEATURE_NMEA2000" == "ON" ]]; then
cmake_args+=(
-DNMEA2000_INCLUDE="${TMP_DIR}/NMEA2000/src"
-DNMEA2000_LIB="${TMP_DIR}/NMEA2000/src/libnmea2000.a"
)
fi
# Per-feature toggles (mirror CMakeLists.txt option() names).
local f val
for f in RTLSDR AIRSPY AIRSPYHF HACKRF HYDRASDR SDRPLAY SOAPYSDR ZMQ NMEA2000 PSQL SQLITE WEBVIEWER; do
val=$(eval "echo \$FEATURE_$f")
cmake_args+=("-D${f}=${val}")
done
cmake .. "${cmake_args[@]}" || error_exit "Failed to run cmake"
make -j"$(nproc)" || error_exit "Failed to build AIS-catcher"
cd ../..
install -m 755 AIS-catcher/build/AIS-catcher /usr/bin/AIS-catcher || error_exit "Failed to install AIS-catcher executable"
}
# Install additional files (plugins, DBMS, README, LICENSE) from source
install_source_additional_files() {
# Ensure the config directory exists before copying files into it.
# Do not rely on mkdir -p "$PLUGIN_DIR" creating the parent as a side-effect.
mkdir -p "$CONFIG_DIR" || error_exit "Failed to create config directory"
log "INFO" "Installing plugins"
mkdir -p "$PLUGIN_DIR" || error_exit "Failed to create plugin directory"
cp "${TMP_DIR}/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 "${TMP_DIR}/AIS-catcher/Source/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 "${TMP_DIR}/AIS-catcher/README.md" "$README_DIR" || error_exit "Failed to copy README"
log "INFO" "Copying LICENSE"
cp "${TMP_DIR}/AIS-catcher/LICENSE" /etc/AIS-catcher/LICENSE || error_exit "Failed to copy LICENSE"
}
################################################################################
# PACKAGE INSTALLATION FUNCTIONS
################################################################################
# Check if a pre-built package is available for this system
# Args:
# $1: system_info string (format: os_version_arch)
# Returns:
# 0 if package found (prints package name), 1 otherwise
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_focal_arm64.deb"
"ais-catcher_ubuntu_jammy_amd64.deb"
"ais-catcher_ubuntu_jammy_arm64.deb"
"ais-catcher_ubuntu_jammy_armhf.deb"
"ais-catcher_ubuntu_noble_amd64.deb"
"ais-catcher_ubuntu_noble_arm64.deb"
"ais-catcher_ubuntu_noble_armhf.deb"
"ais-catcher_ubuntu_plucky_amd64.deb"
"ais-catcher_ubuntu_plucky_arm64.deb"
"ais-catcher_ubuntu_plucky_armhf.deb"
"ais-catcher_ubuntu_questing_amd64.deb"
"ais-catcher_ubuntu_questing_arm64.deb"
"ais-catcher_ubuntu_questing_armhf.deb"
"ais-catcher_ubuntu_resolute_amd64.deb"
"ais-catcher_ubuntu_resolute_arm64.deb"
"ais-catcher_ubuntu_resolute_armhf.deb"
)
for package in "${packages[@]}"; do
if [[ $package == *"$system_info"* ]]; then
echo "$package"
return 0
fi
done
return 1
}
# Download and install a pre-built Debian package by filename.
# Separated from install_package_method so the download/install logic can be
# tested or reused independently of the method-selection layer.
download_and_install_deb() {
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 -fsL -o "$temp_dir/$(basename "$download_url")" "$download_url"; then
rm -rf "$temp_dir"
log "WARN" "Failed to download package"
return 1
fi
# Ensure the downloaded file has correct permissions
chmod 644 "$temp_dir/$package"
# Use --allow-downgrades to handle version conflicts and better error handling
log "INFO" "Installing package and dependencies..."
local install_success=false
if apt-get install -y --allow-downgrades "$temp_dir/$package"; 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
log "WARN" "Failed to install package after all attempts"
return 1
fi
log "INFO" "AIS-catcher package installation completed"
}
# Resolve which .deb package to use for this system and drive download_and_install_deb.
install_package_method() {
local system_info
system_info=$(get_system_info)
log "INFO" "Detected system: $system_info"
# Declare separately so the assignment exit-code is not masked by 'local'.
local package
package=$(check_package_availability "$system_info") || true
if [ -z "$package" ]; then
log "WARN" "No pre-built package available for $system_info"
return 1
fi
log "INFO" "Package available: $package"
# Install minimal dependencies for package installation
log "INFO" "Installing minimal dependencies for package installation"
apt-get install -y curl || error_exit "Failed to install minimal dependencies"
download_and_install_deb "$package" || return 1
log "INFO" "AIS-catcher installation from package completed successfully"
}
################################################################################
# CONFIGURATION FUNCTIONS (Common to both installation methods)
################################################################################
# Create and configure aiscatcher system user
setup_aiscatcher_user() {
if [ "$SKIP_USER" = true ]; then
log "INFO" "Skipping user creation (--no-user flag specified), will run as root"
return 0
fi
log "INFO" "Setting up aiscatcher system user"
# Create system user if it doesn't exist
if ! id -u aiscatcher &>/dev/null; then
log "INFO" "Creating aiscatcher system user"
useradd --system --no-create-home --shell /usr/sbin/nologin aiscatcher || error_exit "Failed to create aiscatcher user"
else
log "INFO" "User aiscatcher already exists"
fi
# Add user to necessary groups for SDR device access
for group in plugdev dialout; do
if getent group "$group" &>/dev/null; then
if ! id -nG aiscatcher | grep -qw "$group"; then
log "INFO" "Adding aiscatcher to $group group"
usermod -a -G "$group" aiscatcher || log "WARN" "Failed to add aiscatcher to $group group"
fi
else
log "WARN" "Group $group does not exist, skipping"
fi
done
}
# Setup configuration
setup_configuration() {
log "INFO" "Setting up configuration files"
mkdir -p "$CONFIG_DIR" || error_exit "Failed to create config directory"
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
}
################################################################################
# SYSTEMD SERVICE FUNCTIONS (Common to both installation methods)
################################################################################
# Install the reboot watchdog poll script
install_reboot_script() {
local script_path="${LIB_DIR}/wait-and-reboot.sh"
mkdir -p "$LIB_DIR" || error_exit "Failed to create lib directory"
cat > "$script_path" << 'SCRIPT'
#!/bin/bash
# Schedule a reboot in 5 minutes; cancel it if AIS-catcher recovers in time.
# Checks every 10 seconds; cancels the pending shutdown if the service comes
# back active (e.g. after manual "systemctl reset-failed + start" intervention).
# To cancel a pending reboot manually: shutdown -c
REBOOT_MINUTES=5
shutdown -r "+${REBOOT_MINUTES}" "AIS-catcher failed repeatedly - rebooting in ${REBOOT_MINUTES} minutes. To cancel: shutdown -c"
for i in $(seq 30); do
sleep 10
if systemctl is-active --quiet ais-catcher.service; then
shutdown -c
exit 0
fi
done
SCRIPT
chmod 755 "$script_path" || error_exit "Failed to set permissions on reboot script"
log "INFO" "Reboot watchdog script installed to ${script_path}"
}
# Setup systemd service
setup_systemd_service() {
log "INFO" "Setting up systemd service"
install_reboot_script
# Write the reboot watchdog unit — fires only when OnFailure is triggered (StartLimitBurst > 0)
# Type=simple: systemd spawns the script and does not wait for it to exit, which avoids
# hitting TimeoutStartSec (90s default) during the 5-minute cancellation window.
cat > "$REBOOT_SERVICE_FILE" << EOF
[Unit]
Description=Reboot system due to repeated AIS-catcher failures
[Service]
Type=simple
ExecStart=${LIB_DIR}/wait-and-reboot.sh
EOF
log "INFO" "Reboot watchdog service written to ${REBOOT_SERVICE_FILE}"
# Preserve existing burst/interval/restart from current service file if present
local burst=0
local interval=0
local restart_policy="always"
if [[ -f "$SERVICE_FILE" ]]; then
burst=$(grep "^StartLimitBurst=" "$SERVICE_FILE" | cut -d= -f2 || echo 0)
interval=$(grep "^StartLimitIntervalSec=" "$SERVICE_FILE" | cut -d= -f2 || echo 0)
local existing_restart
existing_restart=$(grep "^Restart=" "$SERVICE_FILE" | cut -d= -f2 || true)
burst=${burst:-0}
interval=${interval:-0}
[[ "$burst" =~ ^[0-9]+$ ]] || burst=0
[[ "$interval" =~ ^[0-9]+$ ]] || interval=0
restart_policy=${existing_restart:-always}
log "INFO" "Preserving existing service limits: burst=${burst} interval=${interval}s restart=${restart_policy}"
fi
# Apply explicitly requested toggle overrides when combined with an installation
if [[ "$SET_REBOOT" == "true" ]]; then
burst="$REBOOT_BURST"
interval="$REBOOT_INTERVAL"
log "INFO" "Overriding watchdog settings: burst=${burst} interval=${interval}s"
elif [[ "$SET_REBOOT" == "false" ]]; then
burst=0
interval=0
log "INFO" "Disabling watchdog as requested"
fi
if [[ "$SET_AUTO_RESTART" == "true" ]]; then
restart_policy="always"
log "INFO" "Overriding auto-restart: always"
elif [[ "$SET_AUTO_RESTART" == "false" ]]; then
restart_policy="no"
log "INFO" "Disabling auto-restart as requested"
fi
# Build main service file content
# OnFailure= is only meaningful when StartLimitBurst > 0; with burst=0 the service
# restarts indefinitely via Restart=always and OnFailure would fire on every crash.
local on_failure_line=""
if [[ "${burst}" -gt 0 ]]; then
on_failure_line="OnFailure=${REBOOT_SERVICE_NAME}"
fi
local service_content="[Unit]
Description=AIS-catcher Service
After=network.target
${on_failure_line:+${on_failure_line}$'\n'}StartLimitIntervalSec=${interval}
StartLimitBurst=${burst}
[Service]"
# Only add User/Group directives if not skipping user creation
if [ "$SKIP_USER" = false ]; then
service_content+="
User=aiscatcher
Group=aiscatcher
SupplementaryGroups=plugdev dialout"
fi
service_content+="
ExecStart=/usr/bin/AIS-catcher -G system on -o 0 -C ${CONFIG_FILE} @${CMD_FILE}
Restart=${restart_policy}
RestartSec=10
[Install]
WantedBy=multi-user.target"
echo "$service_content" | tee "$SERVICE_FILE" > /dev/null || error_exit "Failed to create service file"
systemctl daemon-reload || error_exit "Failed to reload systemd"
}
# 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
elif systemctl is-failed --quiet "$SERVICE_NAME" 2>/dev/null; then
# Service is in a failed/crash-loop state — it was intended to be running
was_active=true
log "INFO" "$SERVICE_NAME is in failed state, resetting before update"
systemctl reset-failed "$SERVICE_NAME" || log "WARN" "Failed to reset failed state"
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
# Clear any stale failed state from before the update so start isn't blocked
systemctl reset-failed "$SERVICE_NAME" 2>/dev/null || true
if systemctl start "$SERVICE_NAME"; then
log "INFO" "$SERVICE_NAME started successfully"
local started=false
for i in {1..10}; do
systemctl is-active --quiet "$SERVICE_NAME" && { started=true; break; }
sleep 1
done
if [[ "$started" == true ]]; 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
}
################################################################################
# INSTALLATION METHOD FUNCTIONS
################################################################################
# Parse command-line arguments
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--package)
log "INFO" "Package installation requested"
INSTALL_PACKAGE=true
INSTALL_REQUESTED=true
shift
;;
-i|--interactive)
log "INFO" "Interactive feature selection requested (forces source build)"
INTERACTIVE=true
INSTALL_REQUESTED=true
shift
;;
-b|--branch)
if [[ -n "${2:-}" && "${2:0:1}" != "-" ]]; then
BRANCH="$2"
INSTALL_REQUESTED=true
log "INFO" "Will clone branch: ${BRANCH}"
shift 2
else
error_exit "Option -b/--branch requires a branch name argument"
fi
;;
--no-systemd)
log "INFO" "Skipping systemd service installation"
SKIP_SYSTEMD=true
shift
;;
--no-user)
log "INFO" "Skipping user creation, will run as root"
SKIP_USER=true
shift
;;
--set-reboot-on-failure)
SET_REBOOT="true"
if [[ -n "${2:-}" && "${2}" =~ ^[0-9]+$ ]]; then
REBOOT_BURST="$2"
shift
fi
if [[ -n "${2:-}" && "${2}" =~ ^[0-9]+$ ]]; then
REBOOT_INTERVAL="$2"
shift
fi
shift
;;
--unset-reboot-on-failure)
SET_REBOOT="false"
shift
;;