diff --git a/Scripts/RMS_FirstRun.sh b/Scripts/RMS_FirstRun.sh index 9d901a1be..f023a275f 100755 --- a/Scripts/RMS_FirstRun.sh +++ b/Scripts/RMS_FirstRun.sh @@ -205,7 +205,7 @@ fi echo "" echo "Updating to the latest version of RMS..." -bash $RMSUPDATESCRIPT 1 +bash $RMSUPDATESCRIPT echo "" echo "4) Editing the configuration file" diff --git a/Scripts/RMS_Update.sh b/Scripts/RMS_Update.sh index f6fa9e1b0..43ffbd5f8 100755 --- a/Scripts/RMS_Update.sh +++ b/Scripts/RMS_Update.sh @@ -1,219 +1,290 @@ #!/bin/bash -# This script is used for updating the RMS code from GitHub - -# WARNING: The update might fail when new dependencies (libraries) -# are introduced! Further steps might have to be undertaken. - +# This script updates the RMS code from GitHub. +# Includes error handling, retries, and ensures critical files are never lost. +# Directories, files, and variables RMSSOURCEDIR=~/source/RMS - RMSBACKUPDIR=~/.rms_backup - -# File for indicating that the update is in progress +CURRENT_CONFIG="$RMSSOURCEDIR/.config" +CURRENT_MASK="$RMSSOURCEDIR/mask.bmp" +BACKUP_CONFIG="$RMSBACKUPDIR/.config" +BACKUP_MASK="$RMSBACKUPDIR/mask.bmp" +SYSTEM_PACKAGES="$RMSSOURCEDIR/system_packages.txt" UPDATEINPROGRESSFILE=$RMSBACKUPDIR/update_in_progress +LOCKFILE="/tmp/update.lock" +MIN_SPACE_MB=200 # Minimum required space in MB +RETRY_LIMIT=3 + +# Function to check available disk space +check_disk_space() { + local dir=$1 + local required_mb=$2 + + # Get available space in KB and convert to MB + local available_mb=$(df -P "$dir" | awk 'NR==2 {print $4/1024}' | cut -d. -f1) + + if [ "$available_mb" -lt "$required_mb" ]; then + echo "Error: Insufficient disk space in $dir. Need ${required_mb}MB, have ${available_mb}MB" + return 1 + fi + return 0 +} -echo "Updating RMS code..." +# Run space check before anything else +echo "Checking available disk space..." +check_disk_space "$RMSSOURCEDIR" "$MIN_SPACE_MB" || exit 1 -# Make the backup directory -mkdir $RMSBACKUPDIR +# Function to clean up and release the lock on exit +cleanup() { + rm -f "$LOCKFILE" +} -# Check if the update was interrupted while it was in progress -UPDATEINPROGRESS="0" -if [ -f $UPDATEINPROGRESSFILE ]; then - echo "Reading update in progress file..." - UPDATEINPROGRESS=$(cat $UPDATEINPROGRESSFILE) - echo "Update interuption status: $UPDATEINPROGRESS" +# Ensure only one instance of the script runs at a time +if [ -f "$LOCKFILE" ]; then + # Read the PID from the lock file + LOCK_PID=$(cat "$LOCKFILE") + + # Check if the process is still running + if ps -p "$LOCK_PID" > /dev/null 2>&1; then + echo "Another instance of the script is already running. Exiting." + exit 1 + else + echo "Stale lock file found. Removing it and continuing." + rm -f "$LOCKFILE" + fi fi -# If an argument (any) is given, then the config and mask won't be backed up -# Also, don't back up the files if the update script was interrupted the last time -if [ $# -eq 0 ] && [ "$UPDATEINPROGRESS" = "0" ]; then - - echo "Backing up the config and mask..." +# Create a lock file with the current process ID +echo $$ > "$LOCKFILE" +trap cleanup EXIT + +# Retry mechanism for critical file operations +retry_cp() { + local src=$1 + local dest=$2 + local temp_dest="${dest}.tmp" + local retries=0 + + while [ $retries -lt $RETRY_LIMIT ]; do + if cp "$src" "$temp_dest"; then + # Validate the copied file + if diff "$src" "$temp_dest" > /dev/null; then + mv "$temp_dest" "$dest" + return 0 + else + echo "Error: Validation failed. Retrying..." + rm -f "$temp_dest" + fi + else + echo "Error: Copy failed. Retrying..." + rm -f "$temp_dest" + fi + retries=$((retries + 1)) + sleep 1 + done + + echo "Critical Error: Failed to copy $src to $dest after $RETRY_LIMIT retries." + return 1 +} + +# Backup files +backup_files() { + echo "Backing up original files..." - # Back up the config and the mask - cp $RMSSOURCEDIR/.config $RMSBACKUPDIR/. - cp $RMSSOURCEDIR/mask.bmp $RMSBACKUPDIR/. + # Backup .config + if [ -f "$CURRENT_CONFIG" ]; then + if ! retry_cp "$CURRENT_CONFIG" "$BACKUP_CONFIG"; then + echo "Critical Error: Could not back up .config file. Aborting." + exit 1 + fi + else + echo "No original .config found. Generic config will be used." + fi + + # Backup mask.bmp + if [ -f "$CURRENT_MASK" ]; then + if ! retry_cp "$CURRENT_MASK" "$BACKUP_MASK"; then + echo "Critical Error: Could not back up mask.bmp file. Aborting." + exit 1 + fi + else + echo "No original mask.bmp found. Blank mask will be used." + fi +} + +# Restore files +restore_files() { + echo "Restoring configuration and mask files..." + + # Restore .config + if [ -f "$BACKUP_CONFIG" ]; then + if ! retry_cp "$BACKUP_CONFIG" "$CURRENT_CONFIG"; then + echo "Critical Error: Failed to restore .config. Aborting." + exit 1 + fi + else + echo "No backup .config found - a new one will be created by the installation." + fi + + # Restore mask.bmp + if [ -f "$BACKUP_MASK" ]; then + if ! retry_cp "$BACKUP_MASK" "$CURRENT_MASK"; then + echo "Critical Error: Failed to restore mask.bmp. Aborting." + exit 1 + fi + else + echo "No backup mask.bmp found - a new blank mask will be created by the installation." + fi +} +# Ensure the backup directory exists +mkdir -p "$RMSBACKUPDIR" + +# Check if the update was interrupted previously +UPDATEINPROGRESS="0" +if [ -f "$UPDATEINPROGRESSFILE" ]; then + echo "Reading update in progress file..." + UPDATEINPROGRESS=$(cat "$UPDATEINPROGRESSFILE") + echo "Update interruption status: $UPDATEINPROGRESS" fi +# Backup files before any modifications +if [ "$UPDATEINPROGRESS" = "0" ]; then + backup_files +else + echo "Skipping backup due to interrupted update state." +fi -cd $RMSSOURCEDIR +# Change to the RMS source directory +cd "$RMSSOURCEDIR" || { echo "Error: RMS source directory not found. Exiting."; exit 1; } # Activate the virtual environment -source ~/vRMS/bin/activate +if [ -f ~/vRMS/bin/activate ]; then + source ~/vRMS/bin/activate +else + echo "Error: Virtual environment not found. Exiting." + exit 1 +fi -# Remove the build dir +# Perform cleanup operations before updating echo "Removing the build directory..." -rm -r build +rm -rf build -# Perform cleanup before installations -echo "Running pyclean for thorough cleanup..." -pyclean . -v --debris all +echo "Cleaning up Python bytecode files..." +if command -v pyclean >/dev/null 2>&1; then + pyclean . -v --debris all +else + echo "pyclean not found, using basic cleanup..." + # Remove .pyc files + find . -name "*.pyc" -type f -delete + # Remove __pycache__ directories + find . -type d -name "__pycache__" -exec rm -r {} + + # Remove .pyo files if they exist + find . -name "*.pyo" -type f -delete +fi -# Cleanup for *.so files in the repository folder echo "Cleaning up *.so files in the repository..." find . -name "*.so" -type f -delete -# Set the flag indicating that the RMS dir is reset -echo "1" > $UPDATEINPROGRESSFILE - -# Stash the cahnges -git stash - -# Pull new code from github -git pull +# Mark the update as in progress +echo "1" > "$UPDATEINPROGRESSFILE" +# Stash any local changes +echo "Stashing local changes..." +if ! git stash; then + echo "Error: git stash failed. Aborting update." + exit 1 +fi -### Install potentially missing libraries ### - -# Function to check if a package is installed -isInstalled() { - dpkg -s "$1" >/dev/null 2>&1 -} - +# Pull the latest code from GitHub +echo "Pulling latest code from GitHub..." +if ! git pull; then + echo "Error: git pull failed. Aborting update." + exit 1 +fi -# Function to attempt passwordless sudo -tryPasswordlessSudo() { - if sudo -n true 2>/dev/null; then - return 0 +# Create template from the current default config file +if [ -f "$CURRENT_CONFIG" ]; then + echo "Creating config template..." + mv "$CURRENT_CONFIG" "$RMSSOURCEDIR/.configTemplate" + + # Verify the move worked + if [ ! -f "$RMSSOURCEDIR/.configTemplate" ]; then + echo "Warning: Failed to verify config template creation" else - return 1 + echo "Config template created successfully" fi -} +fi +# Install missing dependencies +install_missing_dependencies() { -# Function to prompt for sudo password with timeout -sudoWithTimeout() { - local timeout_duration=30 - local attempts=3 - local prompt="[sudo] password for $USER (timeout in ${timeout_duration}s): " - local sudo_keep_alive_duration=$((timeout_duration / 2)) - - echo "Please enter your sudo password. You have $attempts attempts, with a ${timeout_duration}-second timeout." - - for ((i=1; i<=attempts; i++)); do - # Use read with timeout to get the password securely - read -s -t "$timeout_duration" -p "$prompt" password - echo # Move to a new line after password input - - # Check if password is empty (timeout or Ctrl+D) - if [[ -z "$password" ]]; then - return 1 - fi + if [ ! -f "$SYSTEM_PACKAGES" ]; then + echo "Warning: System packages file not found: $SYSTEM_PACKAGES" + return + fi + + local missing_packages=() + + # ----------------------------------------------------------------------------- + # We store system-level dependencies in a separate file (system_packages.txt) + # so that when RMS_Update pulls new code (including a potentially updated list of packages), + # we can read those new dependencies during the same run — no need to run the update + # script twice. Because the main script is loaded into memory, changing it mid-run + # won't reload it. But updating this separate file allows us to immediately pick + # up any added or changed packages without requiring a second pass. + # ----------------------------------------------------------------------------- + + # Identify missing packages + while read -r pkg; do + # Skip blank lines or commented lines + [[ -z "$pkg" || "$pkg" =~ ^# ]] && continue - # Validate the password - if echo "$password" | sudo -S true 2>/dev/null; then - # Keep sudo token alive in background - (while true; do sudo -v; sleep $sudo_keep_alive_duration; done) & - KEEP_SUDO_PID=$! - return 0 - else - if [ $i -lt $attempts ]; then - echo "Sorry, try again. You have $((attempts - i)) attempts remaining." - else - echo "sudo: $attempts incorrect password attempts" - return 1 - fi + if ! dpkg -s "$pkg" &>/dev/null; then + missing_packages+=("$pkg") fi - done - - # If we've exhausted all attempts - return 1 -} - + done < $SYSTEM_PACKAGES -# List of packages to check/install -packages=( - "gobject-introspection" - "libgirepository1.0-dev" - "gstreamer1.0-libav" - "gstreamer1.0-plugins-bad" -) - -# Check if any package is missing -missing_packages=() -for package in "${packages[@]}"; do - if ! isInstalled "$package"; then - missing_packages+=("$package") + # If no missing packages, inform and return + if [ ${#missing_packages[@]} -eq 0 ]; then + echo "All required packages are already installed." + return fi -done -# If all packages are installed, inform and continue -if [ ${#missing_packages[@]} -eq 0 ]; then - echo "All required packages are already installed." -else - # Some packages are missing, so we need to update and install - echo "The following packages need to be installed: ${missing_packages[*]}" - - # First, try passwordless sudo - if tryPasswordlessSudo; then - echo "Passwordless sudo available. Proceeding with installation." + echo "The following packages are missing and will be installed: ${missing_packages[*]}" + + if sudo -n true 2>/dev/null; then + echo "Passwordless sudo available. Installing missing packages..." sudo apt-get update - all_installed=true for package in "${missing_packages[@]}"; do - echo "Installing $package..." if ! sudo apt-get install -y "$package"; then - echo "Failed to install $package" - all_installed=false + echo "Failed to install $package. Please install it manually." fi done - if $all_installed; then - echo "All required packages have been successfully installed." - else - echo "Some packages failed to install. Please check the output above for details." - fi else - # Passwordless sudo not available, prompt for password - echo "Passwordless sudo not available. Prompting for password." - if ! sudoWithTimeout; then - echo "Password entry timed out or was incorrect. Skipping package installation." - else - # Password entered successfully, proceed with update and install - sudo apt-get update - # Install missing packages - all_installed=true - for package in "${missing_packages[@]}"; do - echo "Installing $package..." - if ! sudo apt-get install -y "$package"; then - echo "Failed to install $package" - all_installed=false - fi - done - if $all_installed; then - echo "All required packages have been successfully installed." - else - echo "Some packages failed to install. Please check the output above for details." + echo "sudo privileges required. Prompting for password." + sudo apt-get update + for package in "${missing_packages[@]}"; do + if ! sudo apt-get install -y "$package"; then + echo "Failed to install $package. Please install it manually." fi - # Kill the background sudo-keeping process - kill $KEEP_SUDO_PID 2>/dev/null - fi + done fi -fi - -### ### - +} +install_missing_dependencies -# make sure the correct requirements are installed +# Install Python requirements pip install -r requirements.txt -# Run the python setup +# Run the Python setup python setup.py install -# Create a template file from the source config and copy the user config and mask files back -if [ $# -eq 0 ]; then - # Rename the existing source .config file to .configTemplate - mv $RMSSOURCEDIR/.config $RMSSOURCEDIR/.configTemplate - - # Copy the user config and mask files back - cp $RMSBACKUPDIR/.config $RMSSOURCEDIR/. - cp $RMSBACKUPDIR/mask.bmp $RMSSOURCEDIR/. -fi - -# Set the flag that the update is not in progress -echo "0" > $UPDATEINPROGRESSFILE +# Restore files after updates +restore_files +# Mark the update as completed +echo "0" > "$UPDATEINPROGRESSFILE" -echo "Update finished! Update exiting in 5 seconds..." -sleep 5 \ No newline at end of file +echo "Update process completed successfully! Exiting in 5 seconds..." +sleep 5 diff --git a/system_packages.txt b/system_packages.txt new file mode 100644 index 000000000..3d619d08a --- /dev/null +++ b/system_packages.txt @@ -0,0 +1,4 @@ +gobject-introspection +libgirepository1.0-dev +gstreamer1.0-libav +gstreamer1.0-plugins-bad \ No newline at end of file