2026-08-06 21:34:52 -05:00
#!/usr/bin/env bash
# TitleBot-NG - Master Launch Script
# Starts all components of the TitleBot system
# THIS SCRIPT WAS CREATED VIA BLACKBOX AI -- HUMANIZATION REQUIRED
set -e
# Color codes for output
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
# Detect OS
detect_os() {
case " $( uname -s) " in
Linux*) echo "linux" ;;
FreeBSD*) echo "freebsd" ;;
*) echo "linux" ;;
esac
}
OS = $( detect_os)
echo -e " ${ BLUE } Detected OS: $OS ${ NC } "
2026-08-07 14:06:18 -05:00
# Ensure Go is installed, attempting native package manager install if missing
ensure_go() {
if command -v go >/dev/null 2>& 1; then
echo -e " ${ GREEN } Go found: $( go version) ${ NC } "
return 0
fi
echo -e " ${ YELLOW } Go not found. Attempting to install via native package manager... ${ NC } "
local pm = ""
if command -v apt-get >/dev/null 2>& 1; then
pm = "apt"
elif command -v pacman >/dev/null 2>& 1; then
pm = "pacman"
elif command -v dnf >/dev/null 2>& 1; then
pm = "dnf"
elif command -v pkg >/dev/null 2>& 1 && [ " $OS " = "freebsd" ] ; then
pm = "pkg"
fi
if [ -z " $pm " ] ; then
echo -e " ${ RED } Error: No supported package manager found (apt/pacman/dnf/pkg). ${ NC } "
echo -e " ${ YELLOW } Please install Go manually: https://go.dev/dl/ ${ NC } "
exit 1
fi
local sudo_cmd = ""
if [ " $( id -u) " -ne 0 ] ; then
if command -v sudo >/dev/null 2>& 1; then
sudo_cmd = "sudo"
else
echo -e " ${ RED } Error: Need root privileges to install Go but sudo is unavailable. ${ NC } "
exit 1
fi
fi
case " $pm " in
apt)
$sudo_cmd apt-get update
$sudo_cmd apt-get install -y golang-go
;;
pacman)
$sudo_cmd pacman -Syu --noconfirm go
;;
dnf)
$sudo_cmd dnf install -y golang
;;
pkg)
$sudo_cmd pkg install -y go
;;
esac
if command -v go >/dev/null 2>& 1; then
echo -e " ${ GREEN } Go installed successfully: $( go version) ${ NC } "
else
echo -e " ${ RED } Error: Go installation failed. Please install Go manually: https://go.dev/dl/ ${ NC } "
exit 1
fi
}
ensure_go
2026-08-06 21:34:52 -05:00
# Set OS-specific binary names
get_bin_name() {
local base = $1
case " $OS " in
freebsd)
case " $base " in
jep) echo "jepfbd" ;;
vs) echo "vsfbsd" ;;
*) echo " ${ base } _ ${ OS } " ;;
esac
;;
*)
echo " $base "
;;
esac
}
JEP_BIN = $( get_bin_name "jep" )
VS_BIN = $( get_bin_name "vs" )
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
cd " $SCRIPT_DIR "
# PID file directory
PID_DIR = " $SCRIPT_DIR /.pids"
mkdir -p " $PID_DIR "
# Log directory
LOG_DIR = " $SCRIPT_DIR /logs"
mkdir -p " $LOG_DIR "
# Cleanup function
cleanup() {
echo -e "\n ${ YELLOW } Shutting down all services... ${ NC } "
# Kill all background processes
if [ -d " $PID_DIR " ] ; then
for pidfile in " $PID_DIR " /*.pid; do
if [ -f " $pidfile " ] ; then
pid = $( cat " $pidfile " )
name = $( basename " $pidfile " .pid)
if kill -0 " $pid " 2>/dev/null; then
echo -e " ${ YELLOW } Stopping $name (PID: $pid ) ${ NC } "
kill " $pid " 2>/dev/null || true
fi
rm -f " $pidfile "
2026-08-12 18:22:26 -05:00
rm -f " $PID_DIR / $name .cmd"
2026-08-06 21:34:52 -05:00
fi
done
fi
# Fallback: kill any lingering service processes even if pid files are stale/missing
for bin in " $JEP_BIN " " $VS_BIN " ; do
pkill -x " $bin " 2>/dev/null || true
done
for script in scraper post_results get_votes get_user_list; do
pkill -f "python3 $script .py" 2>/dev/null || true
done
2026-08-12 19:13:37 -05:00
rm -rf " ${ RESTART_STATE_DIR :- } " 2>/dev/null || true
2026-08-06 21:34:52 -05:00
echo -e " ${ GREEN } All services stopped. ${ NC } "
exit 0
}
# Set up trap for cleanup
trap cleanup SIGINT SIGTERM EXIT
# Function to start a Go service
start_go_service() {
local name = $1
local dir = $2
local binary = $3
2026-08-12 19:13:37 -05:00
local rebuild = 0
2026-08-06 21:34:52 -05:00
2026-08-12 19:13:37 -05:00
if [ -f " $dir / $binary " ] ; then
echo -e " ${ GREEN } Found existing $name binary ( $dir / $binary ), checking freshness... ${ NC } "
# Only skip the build if the binary matches the current OS target and
# is newer than every Go source file in the project (stale binaries rebuild).
local os_ok = 1
case " $OS : $binary " in
freebsd:jepfbd| freebsd:vsfbsd) os_ok = 0 ;;
linux:jep| linux:vs) os_ok = 0 ;;
esac
if [ " $os_ok " -ne 0 ] ; then
echo -e " ${ YELLOW } $binary is for another platform; rebuilding ${ name } for $OS ${ NC } "
rebuild = 1
else
local newest_src
newest_src = $( ( cd " $dir " && ls -t *.go go.mod go.sum 2>/dev/null) | head -1 )
if [ -z " $newest_src " ] || [ " $dir / $binary " -nt " $dir / $newest_src " ] ; then
echo -e " ${ GREEN } ✓ $name binary is current; skipping build ${ NC } "
rebuild = 0
else
echo -e " ${ YELLOW } $name binary is older than ${ newest_src } ; rebuilding ${ NC } "
rebuild = 1
fi
fi
2026-08-06 21:34:52 -05:00
else
2026-08-12 19:13:37 -05:00
rebuild = 1
fi
if [ " $rebuild " = "1" ] ; then
echo -e " ${ BLUE } Building $name ... ${ NC } "
if [ " $OS " = "freebsd" ] ; then
( cd " $dir " && GOOS = freebsd go build -o " $binary " ) || {
echo -e " ${ RED } Failed to build $name for FreeBSD ${ NC } "
return 1
}
else
( cd " $dir " && go build -o " $binary " ) || {
echo -e " ${ RED } Failed to build $name ${ NC } "
return 1
}
fi
2026-08-06 21:34:52 -05:00
fi
echo -e " ${ GREEN } Starting $name ... ${ NC } "
( cd " $dir " && ./" $binary " > " $LOG_DIR / $name .log" 2>& 1) &
echo $! > " $PID_DIR / $name .pid"
2026-08-12 18:22:26 -05:00
echo "(cd \" $dir \" && .\"/ $binary \" > \" $LOG_DIR / $name .log\" 2>&1) &" > " $PID_DIR / $name .cmd"
2026-08-06 21:34:52 -05:00
# Brief wait to check if it started successfully
sleep 1
if kill -0 $( cat " $PID_DIR / $name .pid" ) 2>/dev/null; then
echo -e " ${ GREEN } ✓ $name started (PID: $( cat " $PID_DIR / $name .pid" ) ) ${ NC } "
return 0
else
echo -e " ${ RED } ✗ $name failed to start ${ NC } "
return 1
fi
}
# Function to start a Python service
start_python_service() {
local name = $1
local script = $2
local config_path = ${ 3 :- }
local config_arg = ""
local label = " $name "
if [ -n " $config_path " ] ; then
config_arg = " $config_path "
label = " ${ name } - $( basename " $config_path " .yaml) "
fi
echo -e " ${ GREEN } Starting $label ... ${ NC } "
# Activate virtual environment and run script
2026-08-07 14:06:18 -05:00
( source .venv/bin/activate && python3 " $script " $config_arg > " $LOG_DIR / $label .log" 2>& 1) &
2026-08-06 21:34:52 -05:00
echo $! > " $PID_DIR / $label .pid"
2026-08-12 18:22:26 -05:00
echo "(source .venv/bin/activate && python3 \" $script \" $config_arg > \" $LOG_DIR / $label .log\" 2>&1) &" > " $PID_DIR / $label .cmd"
2026-08-06 21:34:52 -05:00
# Brief wait to check if it started successfully
sleep 1
if kill -0 $( cat " $PID_DIR / $label .pid" ) 2>/dev/null; then
echo -e " ${ GREEN } ✓ $label started (PID: $( cat " $PID_DIR / $label .pid" ) ) ${ NC } "
return 0
else
echo -e " ${ RED } ✗ $label failed to start ${ NC } "
return 1
fi
}
2026-08-07 14:06:18 -05:00
# Ensure Python virtual environment exists, creating it if missing
ensure_venv() {
if [ -d ".venv" ] ; then
echo -e " ${ GREEN } Python virtual environment found at .venv ${ NC } "
return 0
fi
echo -e " ${ YELLOW } Python virtual environment not found. Creating it... ${ NC } "
python3 -m venv .venv || {
echo -e " ${ RED } Error: Failed to create virtual environment. Is python3-venv installed? ${ NC } "
echo -e " ${ YELLOW } Try: sudo apt install python3-venv (or the equivalent for your distro) ${ NC } "
exit 1
}
echo -e " ${ GREEN } Virtual environment created. Installing dependencies... ${ NC } "
if source .venv/bin/activate && pip install matrix-nio pyyaml requests; then
echo -e " ${ GREEN } Dependencies installed successfully. ${ NC } "
else
echo -e " ${ RED } Error: Failed to install Python dependencies. ${ NC } "
exit 1
fi
}
ensure_venv
2026-08-06 21:34:52 -05:00
# Check if config.yaml exists
if [ ! -f "configs/config.yaml" ] ; then
echo -e " ${ RED } Error: configs/config.yaml not found ${ NC } "
echo -e " ${ YELLOW } Please copy configs/config_example.yaml to configs/config.yaml and configure it ${ NC } "
exit 1
fi
# Discover active room configs (skip example/template files)
room_configs =()
for cfg in configs/*.yaml; do
if [[ " $( basename " $cfg " ) " == *example* ]] ; then
continue
fi
room_configs +=( " $cfg " )
done
if [ ${# room_configs [@] } -eq 0 ] ; then
echo -e " ${ RED } Error: no room configs found in configs/*.yaml ${ NC } "
exit 1
fi
echo -e " ${ BLUE } Found ${# room_configs [@] } room config(s): ${ room_configs [*] }${ NC } "
echo -e " ${ BLUE } ======================================== ${ NC } "
echo -e " ${ BLUE } TitleBot-NG - Starting All Services ${ NC } "
echo -e " ${ BLUE } ======================================== ${ NC } "
echo ""
# Start services in order
echo -e " ${ YELLOW } Cleaning up any existing processes... ${ NC } "
for bin in " $JEP_BIN " " $VS_BIN " ; do
if pgrep -x " $bin " > /dev/null 2>& 1; then
echo -e " ${ YELLOW } Stopping existing $bin process... ${ NC } "
pkill -x " $bin " 2>/dev/null || true
fi
done
for script in scraper post_results get_votes get_user_list; do
if pgrep -f "python3 $script .py" > /dev/null 2>& 1; then
echo -e " ${ YELLOW } Stopping existing $script process... ${ NC } "
pkill -f "python3 $script .py" 2>/dev/null || true
fi
done
sleep 1
echo -e " ${ YELLOW } Step 1: Starting scrapers (write room_slugs.json for voting paths)... ${ NC } "
for cfg in " ${ room_configs [@] } " ; do
start_python_service "scraper" "scraper.py" " $cfg "
done
sleep 2
echo -e " ${ YELLOW } Step 2: Starting backend services... ${ NC } "
start_go_service "jsonEndpoint" "jsonEndpoint" " $JEP_BIN " ; true
sleep 2 # Give jsonEndpoint time to fully start
start_go_service "voteServer" "voteServer" " $VS_BIN " ; true
sleep 2 # Give voteServer time to fully start
echo ""
echo -e " ${ YELLOW } Step 3: Starting remaining Matrix bots... ${ NC } "
for cfg in " ${ room_configs [@] } " ; do
start_python_service "post_results" "post_results.py" " $cfg "
sleep 1
start_python_service "get_votes" "get_votes.py" " $cfg "
sleep 1
start_python_service "get_user_list" "get_user_list.py" " $cfg "
done
echo ""
echo -e " ${ GREEN } ======================================== ${ NC } "
echo -e " ${ GREEN } All services started successfully! ${ NC } "
echo -e " ${ GREEN } ======================================== ${ NC } "
echo ""
echo -e " ${ BLUE } Service Status: ${ NC } "
echo -e " • JSON Endpoint: http://localhost:9080"
echo -e " • Vote Server: http://localhost:9081"
for cfg in " ${ room_configs [@] } " ; do
label = $( basename " $cfg " .yaml)
echo -e " • Room ( $label ): scraper / post_results / get_votes / get_user_list"
done
echo ""
echo -e " ${ BLUE } Logs are available in: $LOG_DIR ${ NC } "
echo -e " ${ BLUE } PIDs are stored in: $PID_DIR ${ NC } "
echo ""
echo -e " ${ YELLOW } Press Ctrl+C to stop all services ${ NC } "
echo ""
2026-08-12 19:13:37 -05:00
# Minimum free memory (MB) required before an auto-restart is attempted.
# Prevents making an out-of-memory situation worse by launching new processes.
RESTART_MEM_MIN_MB = ${ RESTART_MEM_MIN_MB :- 128 }
# Max restart attempts per service within the restart window before backing off.
MAX_RESTARTS_PER_WINDOW = ${ MAX_RESTARTS_PER_WINDOW :- 5 }
RESTART_WINDOW_SECS = ${ RESTART_WINDOW_SECS :- 300 }
# Track restart attempts per service (name -> timestamp:count)
RESTART_STATE_DIR = " $PID_DIR /restarts"
mkdir -p " $RESTART_STATE_DIR "
free_mem_mb() {
# Returns approximate free memory in MB (0 if unavailable -> allow restart)
case " $OS " in
freebsd)
local pages free_kb
pages = $( sysctl -n vm.stats.vm.v_free_count 2>/dev/null) || return 0
free_kb = $(( pages * 4096 / 1024 ))
echo " $(( free_kb / 1024 )) "
;;
linux)
awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || return 0
;;
*)
return 0
;;
esac
}
# Register a restart attempt; returns 0 if allowed, 1 if the service is
# restarting too often (crash loop) and should be left down for now.
check_restart_budget() {
local name = $1 now
now = $( date +%s )
local state_file = " $RESTART_STATE_DIR / $name "
# shellcheck disable=SC2086
local count = 0 first = " $now "
if [ -f " $state_file " ] ; then
read -r first count < " $state_file "
fi
# Expire the window once it has passed.
if [ $(( now - first )) -ge " $RESTART_WINDOW_SECS " ] ; then
count = 0
first = $now
fi
if [ " $count " -ge " $MAX_RESTARTS_PER_WINDOW " ] ; then
echo -e " ${ YELLOW } ✗ $name : hit max restarts ( $MAX_RESTARTS_PER_WINDOW in ${ RESTART_WINDOW_SECS } s), leaving down. Clear $RESTART_STATE_DIR / $name to reset. ${ NC } "
return 1
fi
count = $(( count + 1 ))
echo " $first $count " > " $state_file "
return 0
}
2026-08-12 18:22:26 -05:00
# Function to restart a dead service from its saved relaunch command
restart_service() {
local name = $1
local cmd_file = " $PID_DIR / $name .cmd"
if [ ! -f " $cmd_file " ] ; then
echo -e " ${ RED } ✗ $name : no restart command stored, cannot auto-restart ${ NC } "
return 1
fi
2026-08-12 19:13:37 -05:00
if ! check_restart_budget " $name " ; then
return 1
fi
# Do not relaunch more processes if the machine is nearly out of memory.
local free_mb
free_mb = $( free_mem_mb)
if [ -n " $free_mb " ] && [ " $free_mb " -lt " $RESTART_MEM_MIN_MB " ] ; then
echo -e " ${ YELLOW } ✗ $name : only ${ free_mb } MB free (need >= ${ RESTART_MEM_MIN_MB } MB), skipping restart to avoid OOM. ${ NC } "
return 1
fi
2026-08-12 18:22:26 -05:00
echo -e " ${ YELLOW } Restarting $name ... ${ NC } "
rm -f " $PID_DIR / $name .pid"
# shellcheck disable=SC2046
eval " $( cat " $cmd_file " ) "
echo $! > " $PID_DIR / $name .pid"
sleep 1
if kill -0 $( cat " $PID_DIR / $name .pid" ) 2>/dev/null; then
echo -e " ${ GREEN } ✓ $name restarted (PID: $( cat " $PID_DIR / $name .pid" ) ) ${ NC } "
2026-08-12 19:13:37 -05:00
# A restart that survives is a success; reset its budget.
rm -f " $RESTART_STATE_DIR / $name "
2026-08-12 18:22:26 -05:00
return 0
else
echo -e " ${ RED } ✗ $name failed to restart (check $LOG_DIR / $name .log) ${ NC } "
return 1
fi
}
2026-08-06 21:34:52 -05:00
# Keep script running and monitor processes
while true; do
# Check if all processes are still running
all_running = true
for pidfile in " $PID_DIR " /*.pid; do
if [ -f " $pidfile " ] ; then
pid = $( cat " $pidfile " )
if ! kill -0 " $pid " 2>/dev/null; then
name = $( basename " $pidfile " .pid)
echo -e " ${ RED } Warning: $name (PID: $pid ) has stopped unexpectedly ${ NC } "
all_running = false
2026-08-12 18:22:26 -05:00
restart_service " $name "
2026-08-06 21:34:52 -05:00
fi
fi
done
if [ " $all_running " = false ] ; then
2026-08-12 18:22:26 -05:00
echo -e " ${ YELLOW } Some services were restarted. Check logs in $LOG_DIR ${ NC } "
2026-08-06 21:34:52 -05:00
fi
sleep 10
done