Repository: MissMeridian/boiler
Pull request #1: Added support for other Distros
Author: login: M310DYR0CKS
Created: 2025-04-06T23:50:53Z | Merged: 2025-04-07T17:20:35Z

Added support for more then just debian based systems


Diff:
diff --git a/boiler.py b/boiler.py
index 27c5a54..0e9e2ea 100644
--- a/boiler.py
+++ b/boiler.py
@@ -103,4 +103,4 @@ def main():
         time.sleep(20) # Poll every 20 seconds
 
 if __name__ == "__main__":
-    main()
\ No newline at end of file
+    main()
diff --git a/install.sh b/install.sh
index 81f8935..2e2ad9c 100644
--- a/install.sh
+++ b/install.sh
@@ -1,58 +1,57 @@
 #!/usr/bin/env bash
-echo "WARNING: This script will install boiler to a folder in the $HOME directory. You can re-locate this later, but remember to update the systemd services."
+
+echo "WARNING: This script will install boiler to a folder in the \$HOME directory. You can re-locate this later, but remember to update the systemd services."
 read -p "Proceed? (y/n) " -n 1 -r
-if [[ $REPLY =~ ^[Yy]$ ]]
-then
-    cd $HOME # get us where we need to be 
-
-    echo "Updating package repositories"
-    sudo apt-get update
-
-    echo "Checking for git."
-    if ! command -v git &> /dev/null
-    then
-        echo "git not found, installing."
-        sudo apt install -y git
-    else
-        echo "git is installed."
-    fi
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+    cd "$HOME" || exit 1
 
-    echo "Checking for ffmpeg."
-    if ! command -v ffmpeg &> /dev/null
-    then
-        echo "ffmpeg not found, installing."
-        sudo apt install -y ffmpeg
-    else
-        echo "ffmpeg is installed."
-    fi
+    echo "Detecting Linux distribution..."
 
-    echo "Checking for python3, python3-venv, and python3-pip."
-    if ! command -v python3 &> /dev/null
-    then
-        echo "Python 3 is not installed, installing."
-        sudo apt install -y python3 python3-venv python3-pip
+    if [ -f /etc/os-release ]; then
+        . /etc/os-release
+        DISTRO=$ID
     else
-        if ! command -v python3-venv &> /dev/null
-        then
-            echo "Python 3 is installed but python3-venv is missing. Installing."
-            sudo apt install -y python3-venv
-        else
-            echo "python3-venv is installed."
-        fi
-
-        if ! command -v python3-pip &> /dev/null
-        then
-            echo "python3-pip is not installed, installing."
-            sudo apt install -y python3-pip
-        else
-            echo "python3-pip is installed."
-        fi
+        echo "Cannot detect Linux distribution. Please install dependencies manually."
+        exit 1
     fi
 
+    echo "Detected distro: $DISTRO"
+
+    install_packages() {
+        case "$DISTRO" in
+            ubuntu|debian)
+                sudo apt-get update
+                sudo apt-get install -y git ffmpeg python3 python3-venv python3-pip
+                ;;
+            arch|manjaro)
+                sudo pacman -Syu --noconfirm
+                sudo pacman -S --noconfirm git ffmpeg python python-virtualenv python-pip
+                ;;
+            fedora)
+                sudo dnf install -y git ffmpeg python3 python3-virtualenv python3-pip
+                ;;
+            void)
+                sudo xbps-install -S
+                sudo xbps-install -y git ffmpeg python3 python3-pip python3-virtualenv
+                ;;
+            gentoo)
+                sudo emerge --sync
+                sudo emerge dev-vcs/git media-video/ffmpeg dev-lang/python dev-python/pip dev-python/virtualenv
+                ;;
+            *)
+                echo "Unsupported distro: $DISTRO. Please install git, ffmpeg, python3, python3-venv, and python3-pip manually."
+                exit 1
+                ;;
+        esac
+    }
+
+    install_packages
+
     BOILER_DIR=$HOME/boiler
     if [ -d "$BOILER_DIR" ]; then
         echo "Directory $BOILER_DIR already exists. Deleting it..."
-        rm -rf "$BOILER_DIR" # oh god, PLEASE be careful
+        rm -rf "$BOILER_DIR"
     fi
 
     git clone https://github.com/MissMeridian/boiler.git "$BOILER_DIR"
@@ -79,6 +78,7 @@ then
     deactivate
 
     echo "Installing boiler-alerts and boiler-web systemd services..."
+
     cat <<EOL | sudo tee /etc/systemd/system/boiler-alerts.service
 [Unit]
 Description=Boiler - Alerts & Feed Management
@@ -121,7 +121,8 @@ EOL
     echo "Enabling services..."
     sudo systemctl enable boiler-alerts.service
     sudo systemctl enable boiler-web.service
-    systemctl start boiler-alerts boiler-web
+    sudo systemctl start boiler-alerts
+    sudo systemctl start boiler-web
 
     echo "Setup complete! You should see an XML response on http://localhost:8080/IPAWSOPEN_EAS_SERVICE/rest/update"
     echo "Make sure to edit your config and set the root_url to match your server's domain or IP address! You will need to restart boiler-alerts and boiler-web."


Reviews:
user: login: MissMeridian
body: Your changes to boiler.py are mostly redundant and faulty in some cases (removing proper file closure, breaking the config reload, and removing the instance enforcement in load_config). As a result, I won't approve these changes.

Adding distribution support in the install script is a useful feature and I would love to add it. Also thanks for correcting my missing quotes.

Unfortunately because the boiler.py changes are part of this pull request, I can't merge your install.sh to the main branch. If you can make a new request with _just_ the modifications install.sh, I'd be happy to approve them.

Thanks.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-07T03:19:45Z

Review comments:
diff_hunk: @@ -1,106 +1,106 @@
 ## BOILER: CAR to CAP Bridge by CABLE CONTRIBUTES TO LIFE
-import requests, time, json, logging, os, coloredlogs
+import os, json, time, logging, datetime as dt
+import requests, coloredlogs
 import alertProcessor as ap
 import feedManagement as fm
-import webProcess as wp
-import datetime as dt
+import webProcess as wp 
 
-# Logging setup.
-log = logging.getLogger()
-startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
-startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
-if not os.path.exists(f"logs"):
-    os.mkdir("logs")
-if os.path.exists("logs/boiler.log"):
-    os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
-log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-log_formatter = logging.Formatter(log_format)
-FileOutputHandler = logging.FileHandler('logs/boiler.log')
-FileOutputHandler.setFormatter(log_formatter)
-coloredlogs.install(level="INFO")
-log.addHandler(FileOutputHandler)
-log.info(f"Logging started at {startup_time_actual}.")
+# Setup logging
+def setup_logging():
+    log = logging.getLogger()
+    startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
+    startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
+    
+    os.makedirs("logs", exist_ok=True)
+
+    if os.path.exists("logs/boiler.log"):
+        os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
+
+    log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    log_formatter = logging.Formatter(log_format)
+    file_handler = logging.FileHandler("logs/boiler.log")
+    file_handler.setFormatter(log_formatter)
+
+    coloredlogs.install(level="INFO")
+    log.addHandler(file_handler)
+    log.setLevel(logging.INFO)
 
-config = None
+    log.info(f"Logging started at {startup_time_actual}.")
+    return log
+
+log = setup_logging()
+config = {}
 
 def load_config():
-    '''Reloads the config file on startup and at the start of each main loop. Additionally, it will also correct any malformed JSON and reset config objects to default values if objects are missing or incorrect.'''
+    """Reloads and validates the configuration."""
     global config
     log.info("Reloading configuration file.")
+    
+    cfg = {}
     try:
-        with open(f"boiler.cfg", "r") as cfg_file: # Yeah, sorry, this means you need to run this script in its working directory.
+        with open("boiler.cfg", "r") as cfg_file:
             cfg = json.load(cfg_file)
-            cfg_file.close()
     except FileNotFoundError:
-        log.error(f"'boiler.cfg' not found in working directory! Config will be re-written and set to default values.")
-        cfg = {}
-    except json.decoder.JSONDecodeError:
-        log.error(f"'boiler.cfg' was not properly formatted as JSON! Config will be re-written and reset.")
-        cfg = {}
-    except: 
-        log.error(f"Something unexpected happened while trying to load the config file. You can find out more in the traceback below!", exc_info=True)
-    
-    # We'll load the current config values as a new config, if they exist, and if not, the values will be set back to the default.
+        log.error("'boiler.cfg' not found! Creating with default values.")
+    except json.JSONDecodeError:
+        log.error("'boiler.cfg' is malformed! Resetting to defaults.")
+    except Exception:
+        log.exception("Unexpected error while loading 'boiler.cfg'.")
+
     new_cfg = {
-        "poll_url": str(cfg.get("poll_url", "https://alerts.globaleas.org/api/v1/alerts/active")),
-        "alerts_dir": str(cfg.get("alerts_dir", "alerts")),
-        "archive_dir": str(cfg.get("archive_dir", "archive")),
+        "poll_url": cfg.get("poll_url", "https://alerts.globaleas.org/api/v1/alerts/active"),
+        "alerts_dir": cfg.get("alerts_dir", "alerts"),
+        "archive_dir": cfg.get("archive_dir", "archive"),
         "web": {
             "flask": {
-                "enabled": bool(cfg.get("web", {}).get("flask", {}).get("enabled", True)),
-                "host_address": str(cfg.get("web", {}).get("flask", {}).get("host_address", "127.0.0.1")),
-                "host_port": int(cfg.get("web", {}).get("flask", {}).get("host_port", 8080))
+                "enabled": cfg.get("web", {}).get("flask", {}).get("enabled", True),
+                "host_address": cfg.get("web", {}).get("flask", {}).get("host_address", "127.0.0.1"),
+                "host_port": cfg.get("web", {}).get("flask", {}).get("host_port", 8080)
             },
-            "root_url": str(cfg.get("web", {}).get("root_url", "https://your-domain.or.ip-address:port/boiler")),
-            "alerts_suffix": str(cfg.get("web", {}).get("alerts_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/alerts")),
-            "feed_suffix": str(cfg.get("web", {}).get("feed_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/feed")),
-            "update_suffix": str(cfg.get("web", {}).get("update_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/update"))
+            "root_url": cfg.get("web", {}).get("root_url", "https://your-domain.or.ip-address:port/boiler"),
+            "alerts_suffix": cfg.get("web", {}).get("alerts_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/alerts"),
+            "feed_suffix": cfg.get("web", {}).get("feed_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/feed"),
+            "update_suffix": cfg.get("web", {}).get("update_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/update")
         },
         "audio": {
-            "store_local": bool(cfg.get("audio", {}).get("store_local", True)),
-            "trim_headers": bool(cfg.get("audio", {}).get("trim_headers", True))
+            "store_local": cfg.get("audio", {}).get("store_local", True),
+            "trim_headers": cfg.get("audio", {}).get("trim_headers", True)
         },
-        "delete_on_expire": bool(cfg.get("delete_on_expire", True)),
-        "trim_encoder_prefix": bool(cfg.get("trim_encoder_prefix", True))
+        "delete_on_expire": cfg.get("delete_on_expire", True),
+        "trim_encoder_prefix": cfg.get("trim_encoder_prefix", True)
     }
-    if new_cfg != cfg: # This condition would only ever be met if a key was reset to the default value.
-        difference = new_cfg.keys() - cfg.keys()
-        log.warning(f"Some config items were missing or invalid and reset to the default values: {difference}")
-        log.info("Overwriting config file 'boiler.cfg'")
-        with open(f"boiler.cfg", "w") as cfg_file:
-            json.dump(obj=new_cfg, fp=cfg_file, indent=2)
-            cfg_file.close()
+
+    if new_cfg != cfg:
+        log.warning("Missing or invalid config keys detected. Rewriting 'boiler.cfg' with defaults.")
+        with open("boiler.cfg", "w") as cfg_file:
+            json.dump(new_cfg, cfg_file, indent=2)
+
     if config and new_cfg != config:
-        log.info("Config change detected! Updating running config with new values.")
-        config = new_cfg
-    else:
-        log.debug(f"Config has not changed since last run.")
-    if not config:
-        log.debug(f"Initialized config for first-time run.")
-        config = new_cfg
-        
+        log.info("Configuration updated during runtime.")
+    
+    config = new_cfg
 
 def main():
-    ## Main Loop
+    load_config()
path: boiler.py
user: login: MissMeridian
body: By moving the load_config() call outside of the while loop, the config will never be reloaded.
created_at: 2025-04-07T02:30:42Z
author_association: OWNER
diff_hunk: @@ -1,106 +1,106 @@
 ## BOILER: CAR to CAP Bridge by CABLE CONTRIBUTES TO LIFE
-import requests, time, json, logging, os, coloredlogs
+import os, json, time, logging, datetime as dt
+import requests, coloredlogs
 import alertProcessor as ap
 import feedManagement as fm
-import webProcess as wp
-import datetime as dt
+import webProcess as wp 
 
-# Logging setup.
-log = logging.getLogger()
-startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
-startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
-if not os.path.exists(f"logs"):
-    os.mkdir("logs")
-if os.path.exists("logs/boiler.log"):
-    os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
-log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-log_formatter = logging.Formatter(log_format)
-FileOutputHandler = logging.FileHandler('logs/boiler.log')
-FileOutputHandler.setFormatter(log_formatter)
-coloredlogs.install(level="INFO")
-log.addHandler(FileOutputHandler)
-log.info(f"Logging started at {startup_time_actual}.")
+# Setup logging
+def setup_logging():
+    log = logging.getLogger()
+    startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
+    startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
+    
+    os.makedirs("logs", exist_ok=True)
+
+    if os.path.exists("logs/boiler.log"):
+        os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
+
+    log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    log_formatter = logging.Formatter(log_format)
+    file_handler = logging.FileHandler("logs/boiler.log")
+    file_handler.setFormatter(log_formatter)
+
+    coloredlogs.install(level="INFO")
+    log.addHandler(file_handler)
+    log.setLevel(logging.INFO)
 
-config = None
+    log.info(f"Logging started at {startup_time_actual}.")
+    return log
+
+log = setup_logging()
path: boiler.py
user: login: MissMeridian
body: I would understand why you put the logger setup into its own function, however, you never call this function anywhere other than in the root of the script, effectively doing the same thing as the original. This is redundant.
created_at: 2025-04-07T02:34:40Z
author_association: OWNER
diff_hunk: @@ -1,106 +1,106 @@
 ## BOILER: CAR to CAP Bridge by CABLE CONTRIBUTES TO LIFE
-import requests, time, json, logging, os, coloredlogs
+import os, json, time, logging, datetime as dt
+import requests, coloredlogs
 import alertProcessor as ap
 import feedManagement as fm
-import webProcess as wp
-import datetime as dt
+import webProcess as wp 
 
-# Logging setup.
-log = logging.getLogger()
-startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
-startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
-if not os.path.exists(f"logs"):
-    os.mkdir("logs")
-if os.path.exists("logs/boiler.log"):
-    os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
-log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-log_formatter = logging.Formatter(log_format)
-FileOutputHandler = logging.FileHandler('logs/boiler.log')
-FileOutputHandler.setFormatter(log_formatter)
-coloredlogs.install(level="INFO")
-log.addHandler(FileOutputHandler)
-log.info(f"Logging started at {startup_time_actual}.")
+# Setup logging
+def setup_logging():
+    log = logging.getLogger()
+    startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
+    startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
+    
+    os.makedirs("logs", exist_ok=True)
+
+    if os.path.exists("logs/boiler.log"):
+        os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
+
+    log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    log_formatter = logging.Formatter(log_format)
+    file_handler = logging.FileHandler("logs/boiler.log")
+    file_handler.setFormatter(log_formatter)
+
+    coloredlogs.install(level="INFO")
+    log.addHandler(file_handler)
+    log.setLevel(logging.INFO)
 
-config = None
+    log.info(f"Logging started at {startup_time_actual}.")
+    return log
+
+log = setup_logging()
+config = {}
 
 def load_config():
-    '''Reloads the config file on startup and at the start of each main loop. Additionally, it will also correct any malformed JSON and reset config objects to default values if objects are missing or incorrect.'''
+    """Reloads and validates the configuration."""
     global config
     log.info("Reloading configuration file.")
+    
+    cfg = {}
     try:
-        with open(f"boiler.cfg", "r") as cfg_file: # Yeah, sorry, this means you need to run this script in its working directory.
+        with open("boiler.cfg", "r") as cfg_file:
             cfg = json.load(cfg_file)
-            cfg_file.close()
path: boiler.py
user: login: MissMeridian
body: Removing the file closure can cause write access issues if the file doesn't get properly closed. This one's not so severe because the file is opened here with only read access, but the config file does get opened with write access below.
created_at: 2025-04-07T02:38:00Z
author_association: OWNER
diff_hunk: @@ -1,106 +1,106 @@
 ## BOILER: CAR to CAP Bridge by CABLE CONTRIBUTES TO LIFE
-import requests, time, json, logging, os, coloredlogs
+import os, json, time, logging, datetime as dt
+import requests, coloredlogs
 import alertProcessor as ap
 import feedManagement as fm
-import webProcess as wp
-import datetime as dt
+import webProcess as wp 
 
-# Logging setup.
-log = logging.getLogger()
-startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
-startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
-if not os.path.exists(f"logs"):
-    os.mkdir("logs")
-if os.path.exists("logs/boiler.log"):
-    os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
-log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-log_formatter = logging.Formatter(log_format)
-FileOutputHandler = logging.FileHandler('logs/boiler.log')
-FileOutputHandler.setFormatter(log_formatter)
-coloredlogs.install(level="INFO")
-log.addHandler(FileOutputHandler)
-log.info(f"Logging started at {startup_time_actual}.")
+# Setup logging
+def setup_logging():
+    log = logging.getLogger()
+    startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
+    startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
+    
+    os.makedirs("logs", exist_ok=True)
+
+    if os.path.exists("logs/boiler.log"):
+        os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
+
+    log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    log_formatter = logging.Formatter(log_format)
+    file_handler = logging.FileHandler("logs/boiler.log")
+    file_handler.setFormatter(log_formatter)
+
+    coloredlogs.install(level="INFO")
+    log.addHandler(file_handler)
+    log.setLevel(logging.INFO)
 
-config = None
+    log.info(f"Logging started at {startup_time_actual}.")
+    return log
+
+log = setup_logging()
+config = {}
 
 def load_config():
-    '''Reloads the config file on startup and at the start of each main loop. Additionally, it will also correct any malformed JSON and reset config objects to default values if objects are missing or incorrect.'''
+    """Reloads and validates the configuration."""
     global config
     log.info("Reloading configuration file.")
+    
+    cfg = {}
     try:
-        with open(f"boiler.cfg", "r") as cfg_file: # Yeah, sorry, this means you need to run this script in its working directory.
+        with open("boiler.cfg", "r") as cfg_file:
             cfg = json.load(cfg_file)
-            cfg_file.close()
     except FileNotFoundError:
-        log.error(f"'boiler.cfg' not found in working directory! Config will be re-written and set to default values.")
-        cfg = {}
-    except json.decoder.JSONDecodeError:
-        log.error(f"'boiler.cfg' was not properly formatted as JSON! Config will be re-written and reset.")
-        cfg = {}
-    except: 
-        log.error(f"Something unexpected happened while trying to load the config file. You can find out more in the traceback below!", exc_info=True)
-    
-    # We'll load the current config values as a new config, if they exist, and if not, the values will be set back to the default.
+        log.error("'boiler.cfg' not found! Creating with default values.")
+    except json.JSONDecodeError:
+        log.error("'boiler.cfg' is malformed! Resetting to defaults.")
+    except Exception:
+        log.exception("Unexpected error while loading 'boiler.cfg'.")
+
     new_cfg = {
-        "poll_url": str(cfg.get("poll_url", "https://alerts.globaleas.org/api/v1/alerts/active")),
path: boiler.py
user: login: MissMeridian
body: The instance enforcement was intentionally put here and on all the other config variables to prevent users from setting a value in the config file to another instance type, as it could cause exceptions later on in the program.
created_at: 2025-04-07T02:41:39Z
author_association: OWNER
diff_hunk: @@ -1,106 +1,106 @@
 ## BOILER: CAR to CAP Bridge by CABLE CONTRIBUTES TO LIFE
-import requests, time, json, logging, os, coloredlogs
+import os, json, time, logging, datetime as dt
+import requests, coloredlogs
 import alertProcessor as ap
 import feedManagement as fm
-import webProcess as wp
-import datetime as dt
+import webProcess as wp 
 
-# Logging setup.
-log = logging.getLogger()
-startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
-startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
-if not os.path.exists(f"logs"):
-    os.mkdir("logs")
-if os.path.exists("logs/boiler.log"):
-    os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
-log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-log_formatter = logging.Formatter(log_format)
-FileOutputHandler = logging.FileHandler('logs/boiler.log')
-FileOutputHandler.setFormatter(log_formatter)
-coloredlogs.install(level="INFO")
-log.addHandler(FileOutputHandler)
-log.info(f"Logging started at {startup_time_actual}.")
+# Setup logging
+def setup_logging():
+    log = logging.getLogger()
+    startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
+    startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
+    
+    os.makedirs("logs", exist_ok=True)
+
+    if os.path.exists("logs/boiler.log"):
+        os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
+
+    log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    log_formatter = logging.Formatter(log_format)
+    file_handler = logging.FileHandler("logs/boiler.log")
+    file_handler.setFormatter(log_formatter)
+
+    coloredlogs.install(level="INFO")
+    log.addHandler(file_handler)
+    log.setLevel(logging.INFO)
 
-config = None
+    log.info(f"Logging started at {startup_time_actual}.")
+    return log
+
+log = setup_logging()
+config = {}
 
 def load_config():
-    '''Reloads the config file on startup and at the start of each main loop. Additionally, it will also correct any malformed JSON and reset config objects to default values if objects are missing or incorrect.'''
+    """Reloads and validates the configuration."""
     global config
     log.info("Reloading configuration file.")
+    
+    cfg = {}
     try:
-        with open(f"boiler.cfg", "r") as cfg_file: # Yeah, sorry, this means you need to run this script in its working directory.
+        with open("boiler.cfg", "r") as cfg_file:
             cfg = json.load(cfg_file)
-            cfg_file.close()
     except FileNotFoundError:
-        log.error(f"'boiler.cfg' not found in working directory! Config will be re-written and set to default values.")
-        cfg = {}
-    except json.decoder.JSONDecodeError:
-        log.error(f"'boiler.cfg' was not properly formatted as JSON! Config will be re-written and reset.")
-        cfg = {}
-    except: 
-        log.error(f"Something unexpected happened while trying to load the config file. You can find out more in the traceback below!", exc_info=True)
-    
-    # We'll load the current config values as a new config, if they exist, and if not, the values will be set back to the default.
+        log.error("'boiler.cfg' not found! Creating with default values.")
+    except json.JSONDecodeError:
+        log.error("'boiler.cfg' is malformed! Resetting to defaults.")
+    except Exception:
+        log.exception("Unexpected error while loading 'boiler.cfg'.")
+
     new_cfg = {
-        "poll_url": str(cfg.get("poll_url", "https://alerts.globaleas.org/api/v1/alerts/active")),
-        "alerts_dir": str(cfg.get("alerts_dir", "alerts")),
-        "archive_dir": str(cfg.get("archive_dir", "archive")),
+        "poll_url": cfg.get("poll_url", "https://alerts.globaleas.org/api/v1/alerts/active"),
+        "alerts_dir": cfg.get("alerts_dir", "alerts"),
+        "archive_dir": cfg.get("archive_dir", "archive"),
         "web": {
             "flask": {
-                "enabled": bool(cfg.get("web", {}).get("flask", {}).get("enabled", True)),
-                "host_address": str(cfg.get("web", {}).get("flask", {}).get("host_address", "127.0.0.1")),
-                "host_port": int(cfg.get("web", {}).get("flask", {}).get("host_port", 8080))
+                "enabled": cfg.get("web", {}).get("flask", {}).get("enabled", True),
+                "host_address": cfg.get("web", {}).get("flask", {}).get("host_address", "127.0.0.1"),
+                "host_port": cfg.get("web", {}).get("flask", {}).get("host_port", 8080)
             },
-            "root_url": str(cfg.get("web", {}).get("root_url", "https://your-domain.or.ip-address:port/boiler")),
-            "alerts_suffix": str(cfg.get("web", {}).get("alerts_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/alerts")),
-            "feed_suffix": str(cfg.get("web", {}).get("feed_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/feed")),
-            "update_suffix": str(cfg.get("web", {}).get("update_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/update"))
+            "root_url": cfg.get("web", {}).get("root_url", "https://your-domain.or.ip-address:port/boiler"),
+            "alerts_suffix": cfg.get("web", {}).get("alerts_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/alerts"),
+            "feed_suffix": cfg.get("web", {}).get("feed_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/feed"),
+            "update_suffix": cfg.get("web", {}).get("update_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/update")
         },
         "audio": {
-            "store_local": bool(cfg.get("audio", {}).get("store_local", True)),
-            "trim_headers": bool(cfg.get("audio", {}).get("trim_headers", True))
+            "store_local": cfg.get("audio", {}).get("store_local", True),
+            "trim_headers": cfg.get("audio", {}).get("trim_headers", True)
         },
-        "delete_on_expire": bool(cfg.get("delete_on_expire", True)),
-        "trim_encoder_prefix": bool(cfg.get("trim_encoder_prefix", True))
+        "delete_on_expire": cfg.get("delete_on_expire", True),
+        "trim_encoder_prefix": cfg.get("trim_encoder_prefix", True)
     }
-    if new_cfg != cfg: # This condition would only ever be met if a key was reset to the default value.
-        difference = new_cfg.keys() - cfg.keys()
-        log.warning(f"Some config items were missing or invalid and reset to the default values: {difference}")
-        log.info("Overwriting config file 'boiler.cfg'")
-        with open(f"boiler.cfg", "w") as cfg_file:
-            json.dump(obj=new_cfg, fp=cfg_file, indent=2)
-            cfg_file.close()
+
+    if new_cfg != cfg:
+        log.warning("Missing or invalid config keys detected. Rewriting 'boiler.cfg' with defaults.")
+        with open("boiler.cfg", "w") as cfg_file:
+            json.dump(new_cfg, cfg_file, indent=2)
+
     if config and new_cfg != config:
-        log.info("Config change detected! Updating running config with new values.")
-        config = new_cfg
-    else:
-        log.debug(f"Config has not changed since last run.")
-    if not config:
-        log.debug(f"Initialized config for first-time run.")
-        config = new_cfg
-        
+        log.info("Configuration updated during runtime.")
+    
+    config = new_cfg
path: boiler.py
user: login: MissMeridian
body: Optimal change, yes. The reason I had the previous layout for setting config to new_cfg was for the debug messages.
created_at: 2025-04-07T02:44:32Z
author_association: OWNER
diff_hunk: @@ -1,106 +1,106 @@
 ## BOILER: CAR to CAP Bridge by CABLE CONTRIBUTES TO LIFE
-import requests, time, json, logging, os, coloredlogs
+import os, json, time, logging, datetime as dt
+import requests, coloredlogs
 import alertProcessor as ap
 import feedManagement as fm
-import webProcess as wp
-import datetime as dt
+import webProcess as wp 
 
-# Logging setup.
-log = logging.getLogger()
-startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
-startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
-if not os.path.exists(f"logs"):
-    os.mkdir("logs")
-if os.path.exists("logs/boiler.log"):
-    os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
-log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-log_formatter = logging.Formatter(log_format)
-FileOutputHandler = logging.FileHandler('logs/boiler.log')
-FileOutputHandler.setFormatter(log_formatter)
-coloredlogs.install(level="INFO")
-log.addHandler(FileOutputHandler)
-log.info(f"Logging started at {startup_time_actual}.")
+# Setup logging
+def setup_logging():
+    log = logging.getLogger()
+    startup_time = dt.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
+    startup_time_actual = dt.datetime.now().strftime("%H:%M:%S %m/%d/%Y")
+    
+    os.makedirs("logs", exist_ok=True)
+
+    if os.path.exists("logs/boiler.log"):
+        os.rename("logs/boiler.log", f"logs/boiler-{startup_time}.log")
+
+    log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    log_formatter = logging.Formatter(log_format)
+    file_handler = logging.FileHandler("logs/boiler.log")
+    file_handler.setFormatter(log_formatter)
+
+    coloredlogs.install(level="INFO")
+    log.addHandler(file_handler)
+    log.setLevel(logging.INFO)
 
-config = None
+    log.info(f"Logging started at {startup_time_actual}.")
+    return log
+
+log = setup_logging()
+config = {}
 
 def load_config():
-    '''Reloads the config file on startup and at the start of each main loop. Additionally, it will also correct any malformed JSON and reset config objects to default values if objects are missing or incorrect.'''
+    """Reloads and validates the configuration."""
     global config
     log.info("Reloading configuration file.")
+    
+    cfg = {}
     try:
-        with open(f"boiler.cfg", "r") as cfg_file: # Yeah, sorry, this means you need to run this script in its working directory.
+        with open("boiler.cfg", "r") as cfg_file:
             cfg = json.load(cfg_file)
-            cfg_file.close()
     except FileNotFoundError:
-        log.error(f"'boiler.cfg' not found in working directory! Config will be re-written and set to default values.")
-        cfg = {}
-    except json.decoder.JSONDecodeError:
-        log.error(f"'boiler.cfg' was not properly formatted as JSON! Config will be re-written and reset.")
-        cfg = {}
-    except: 
-        log.error(f"Something unexpected happened while trying to load the config file. You can find out more in the traceback below!", exc_info=True)
-    
-    # We'll load the current config values as a new config, if they exist, and if not, the values will be set back to the default.
+        log.error("'boiler.cfg' not found! Creating with default values.")
+    except json.JSONDecodeError:
+        log.error("'boiler.cfg' is malformed! Resetting to defaults.")
+    except Exception:
+        log.exception("Unexpected error while loading 'boiler.cfg'.")
+
     new_cfg = {
-        "poll_url": str(cfg.get("poll_url", "https://alerts.globaleas.org/api/v1/alerts/active")),
-        "alerts_dir": str(cfg.get("alerts_dir", "alerts")),
-        "archive_dir": str(cfg.get("archive_dir", "archive")),
+        "poll_url": cfg.get("poll_url", "https://alerts.globaleas.org/api/v1/alerts/active"),
+        "alerts_dir": cfg.get("alerts_dir", "alerts"),
+        "archive_dir": cfg.get("archive_dir", "archive"),
         "web": {
             "flask": {
-                "enabled": bool(cfg.get("web", {}).get("flask", {}).get("enabled", True)),
-                "host_address": str(cfg.get("web", {}).get("flask", {}).get("host_address", "127.0.0.1")),
-                "host_port": int(cfg.get("web", {}).get("flask", {}).get("host_port", 8080))
+                "enabled": cfg.get("web", {}).get("flask", {}).get("enabled", True),
+                "host_address": cfg.get("web", {}).get("flask", {}).get("host_address", "127.0.0.1"),
+                "host_port": cfg.get("web", {}).get("flask", {}).get("host_port", 8080)
             },
-            "root_url": str(cfg.get("web", {}).get("root_url", "https://your-domain.or.ip-address:port/boiler")),
-            "alerts_suffix": str(cfg.get("web", {}).get("alerts_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/alerts")),
-            "feed_suffix": str(cfg.get("web", {}).get("feed_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/feed")),
-            "update_suffix": str(cfg.get("web", {}).get("update_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/update"))
+            "root_url": cfg.get("web", {}).get("root_url", "https://your-domain.or.ip-address:port/boiler"),
+            "alerts_suffix": cfg.get("web", {}).get("alerts_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/alerts"),
+            "feed_suffix": cfg.get("web", {}).get("feed_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/feed"),
+            "update_suffix": cfg.get("web", {}).get("update_suffix", "/IPAWSOPEN_EAS_SERVICE/rest/update")
         },
         "audio": {
-            "store_local": bool(cfg.get("audio", {}).get("store_local", True)),
-            "trim_headers": bool(cfg.get("audio", {}).get("trim_headers", True))
+            "store_local": cfg.get("audio", {}).get("store_local", True),
+            "trim_headers": cfg.get("audio", {}).get("trim_headers", True)
         },
-        "delete_on_expire": bool(cfg.get("delete_on_expire", True)),
-        "trim_encoder_prefix": bool(cfg.get("trim_encoder_prefix", True))
+        "delete_on_expire": cfg.get("delete_on_expire", True),
+        "trim_encoder_prefix": cfg.get("trim_encoder_prefix", True)
     }
-    if new_cfg != cfg: # This condition would only ever be met if a key was reset to the default value.
-        difference = new_cfg.keys() - cfg.keys()
-        log.warning(f"Some config items were missing or invalid and reset to the default values: {difference}")
-        log.info("Overwriting config file 'boiler.cfg'")
-        with open(f"boiler.cfg", "w") as cfg_file:
-            json.dump(obj=new_cfg, fp=cfg_file, indent=2)
-            cfg_file.close()
path: boiler.py
user: login: MissMeridian
body: Closing the file is proper as to not prevent any write access issues, especially on multi-user systems like Linux.
created_at: 2025-04-07T02:45:25Z
author_association: OWNER
diff_hunk: @@ -61,12 +60,12 @@ then
 
     if [ ! -d "$VENV_DIR" ]; then
         echo "Creating virtual environment in $VENV_DIR..."
-        python3 -m venv $VENV_DIR
+        python3 -m venv "$VENV_DIR"
path: install.sh
user: login: MissMeridian
body: Good catch. I suppose I wasn't very careful.
created_at: 2025-04-07T02:51:24Z
author_association: OWNER
diff_hunk: @@ -61,12 +60,12 @@ then
 
     if [ ! -d "$VENV_DIR" ]; then
         echo "Creating virtual environment in $VENV_DIR..."
-        python3 -m venv $VENV_DIR
+        python3 -m venv "$VENV_DIR"
     else
         echo "Virtual environment already exists....? But how?"
     fi
 
-    source $VENV_DIR/bin/activate
+    source "$VENV_DIR/bin/activate"
path: install.sh
user: login: MissMeridian
body: Ditto.
created_at: 2025-04-07T02:52:01Z
author_association: OWNER
diff_hunk: @@ -121,8 +121,9 @@ EOL
     echo "Enabling services..."
     sudo systemctl enable boiler-alerts.service
     sudo systemctl enable boiler-web.service
-    systemctl start boiler-alerts boiler-web
+    sudo systemctl start boiler-alerts
+    sudo systemctl start boiler-web
path: install.sh
user: login: MissMeridian
body: Unless other distros can't start multiple systemctl services at once, this is redundant. I've only ever used Debian.
created_at: 2025-04-07T02:59:19Z
author_association: OWNER
path: boiler.py
user: login: MissMeridian
body: Most of the changes here are redundant and unnecessary.

Otherwise, some of the changes introduce potential problems:
- You removed proper file closure, which could cause write access issues.
- You removed the instance enforcement in load_config(), which could cause exceptions later in the program if a user sets a config key to the incorrect instance.
- You removed the load_config() call (which is responsible for reloading the config) from the start of the while loop, meaning that the config won't be reloaded the next time that boiler polls and processes.
created_at: 2025-04-07T03:06:51Z
author_association: OWNER
path: install.sh
user: login: MissMeridian
body: Thanks for building in OS detection type for other Linux distributions, and thanks for finding a potential problem where the boiler directory and venv directory are not wrapped in quotes. I'll fix those lines in my main branch while you have the chance to read my feedback.
created_at: 2025-04-07T03:09:20Z
author_association: OWNER

PR comments:
user: login: M310DYR0CKS
created_at: 2025-04-07T03:50:09Z
body: I reverted the changes made to the boiler.py i didn't mean for them to be made into this pr it was meant for a brach on my fork
author_association: CONTRIBUTOR
user: login: M310DYR0CKS
created_at: 2025-04-07T06:47:24Z
body: @MissMeridian 
author_association: CONTRIBUTOR